summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorKamil Trzcinski <ayufan@ayufan.eu>2017-01-24 17:52:50 +0100
committerKamil Trzcinski <ayufan@ayufan.eu>2017-01-24 17:52:50 +0100
commit3cd17c9430c7575b0c1f1041947f3cd0d991f00c (patch)
tree12638383d600ae6d5c8cbf6c27da7bdb2199f5e7 /app
parent8faabdf7d33b575de11b043cfe6698021d33a973 (diff)
parent8c0e358a972ca9cb2176ff9289a5a89b0c909a93 (diff)
downloadgitlab-ce-3cd17c9430c7575b0c1f1041947f3cd0d991f00c.tar.gz
Merge remote-tracking branch 'origin/master' into 24147-delete-env-button
Diffstat (limited to 'app')
-rw-r--r--app/assets/fonts/OFL.txt93
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttf.woffbin113800 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttf.woff2bin82052 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttf.woffbin49704 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2bin34812 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttf.woffbin117872 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttf.woff2bin85604 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttf.woffbin50608 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2bin35864 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttf.woffbin114336 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2bin82808 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woffbin49684 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2bin34560 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttf.woffbin51012 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttf.woff2bin36016 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttf.woffbin118284 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttf.woff2bin86336 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttf.woffbin50992 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttf.woff2bin35952 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttf.woffbin119064 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttf.woff2bin86844 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttf.woffbin118412 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttf.woff2bin86196 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woffbin50924 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2bin35984 -> 0 bytes
-rw-r--r--app/assets/images/auth_buttons/authentiq_64.pngbin0 -> 17679 bytes
-rw-r--r--app/assets/javascripts/abuse_reports.js.es63
-rw-r--r--app/assets/javascripts/activities.js37
-rw-r--r--app/assets/javascripts/activities.js.es637
-rw-r--r--app/assets/javascripts/admin.js6
-rw-r--r--app/assets/javascripts/api.js19
-rw-r--r--app/assets/javascripts/application.js49
-rw-r--r--app/assets/javascripts/aside.js4
-rw-r--r--app/assets/javascripts/autosave.js4
-rw-r--r--app/assets/javascripts/awards_handler.js16
-rw-r--r--app/assets/javascripts/behaviors/autosize.js4
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.js3
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js4
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js3
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js2
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.es66
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selector.js.es618
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selectors.js.es627
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js6
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js5
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js6
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js5
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.es65
-rw-r--r--app/assets/javascripts/blob/template_selector.js.es6165
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js6
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js14
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es623
-rw-r--r--app/assets/javascripts/boards/components/board.js.es648
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es69
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es611
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js.es64
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es644
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es623
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es612
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es6100
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js.es63
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es611
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es617
-rw-r--r--app/assets/javascripts/boards/models/label.js.es65
-rw-r--r--app/assets/javascripts/boards/models/list.js.es632
-rw-r--r--app/assets/javascripts/boards/models/milestone.js.es67
-rw-r--r--app/assets/javascripts/boards/models/user.js.es67
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es610
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es631
-rw-r--r--app/assets/javascripts/boards/test_utils/simulate_drag.js231
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es66
-rw-r--r--app/assets/javascripts/breakpoints.js8
-rw-r--r--app/assets/javascripts/broadcast_message.js3
-rw-r--r--app/assets/javascripts/build.js156
-rw-r--r--app/assets/javascripts/build_artifacts.js4
-rw-r--r--app/assets/javascripts/build_variables.js.es67
-rw-r--r--app/assets/javascripts/ci_lint_editor.js.es618
-rw-r--r--app/assets/javascripts/commit.js6
-rw-r--r--app/assets/javascripts/commit/file.js8
-rw-r--r--app/assets/javascripts/commit/image_file.js6
-rw-r--r--app/assets/javascripts/commits.js14
-rw-r--r--app/assets/javascripts/compare.js4
-rw-r--r--app/assets/javascripts/compare_autocomplete.js.es613
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js4
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js8
-rw-r--r--app/assets/javascripts/create_label.js.es64
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es645
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es647
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es644
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es647
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es657
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es644
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es644
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.js.es625
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6217
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es641
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es694
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es67
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es67
-rw-r--r--app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es67
-rw-r--r--app/assets/javascripts/diff.js77
-rw-r--r--app/assets/javascripts/diff.js.es6121
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es619
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es632
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es629
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es69
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es614
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es650
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js.es67
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js.es618
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js.es67
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es612
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js.es67
-rw-r--r--app/assets/javascripts/dispatcher.js.es6118
-rw-r--r--app/assets/javascripts/droplab/droplab.js710
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js79
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax_filter.js145
-rw-r--r--app/assets/javascripts/droplab/droplab_filter.js60
-rw-r--r--app/assets/javascripts/dropzone_input.js7
-rw-r--r--app/assets/javascripts/due_date_select.js.es636
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es6223
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js.es649
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js.es622
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es6537
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js.es632
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js.es626
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js.es627
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es623
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js.es624
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js.es6190
-rw-r--r--app/assets/javascripts/environments/vue_resource_interceptor.js.es612
-rw-r--r--app/assets/javascripts/extensions/array.js8
-rw-r--r--app/assets/javascripts/extensions/array.js.es624
-rw-r--r--app/assets/javascripts/extensions/custom_event.js.es612
-rw-r--r--app/assets/javascripts/extensions/element.js.es619
-rw-r--r--app/assets/javascripts/extensions/jquery.js3
-rw-r--r--app/assets/javascripts/extensions/object.js.es626
-rw-r--r--app/assets/javascripts/files_comment_button.js8
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js.es669
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js.es644
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js.es653
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js.es6120
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js7
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6112
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6207
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es6216
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es683
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es645
-rw-r--r--app/assets/javascripts/flash.js4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es6287
-rw-r--r--app/assets/javascripts/gl_dropdown.js72
-rw-r--r--app/assets/javascripts/gl_field_errors.js.es63
-rw-r--r--app/assets/javascripts/gl_form.js12
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js2
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js4
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js9
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js10
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js16
-rw-r--r--app/assets/javascripts/group_avatar.js8
-rw-r--r--app/assets/javascripts/group_label_subscription.js.es653
-rw-r--r--app/assets/javascripts/groups_select.js8
-rw-r--r--app/assets/javascripts/header.js4
-rw-r--r--app/assets/javascripts/importer_status.js8
-rw-r--r--app/assets/javascripts/issuable.js.es625
-rw-r--r--app/assets/javascripts/issuable/issuable_bundle.js.es61
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es642
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es669
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es613
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/help_state.js.es624
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es611
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es613
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6118
-rw-r--r--app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es661
-rw-r--r--app/assets/javascripts/issuable_context.js6
-rw-r--r--app/assets/javascripts/issuable_form.js15
-rw-r--r--app/assets/javascripts/issue.js10
-rw-r--r--app/assets/javascripts/issue_status_select.js4
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es699
-rw-r--r--app/assets/javascripts/label_manager.js.es67
-rw-r--r--app/assets/javascripts/labels.js6
-rw-r--r--app/assets/javascripts/labels_select.js154
-rw-r--r--app/assets/javascripts/layout_nav.js12
-rw-r--r--app/assets/javascripts/lib/ace.js1
-rw-r--r--app/assets/javascripts/lib/chart.js2
-rw-r--r--app/assets/javascripts/lib/cropper.js2
-rw-r--r--app/assets/javascripts/lib/d3.js2
-rw-r--r--app/assets/javascripts/lib/raphael.js2
-rw-r--r--app/assets/javascripts/lib/utils/animate.js3
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6113
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js130
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es6163
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js14
-rw-r--r--app/assets/javascripts/lib/utils/notify.js4
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js.es665
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js60
-rw-r--r--app/assets/javascripts/lib/utils/timeago.js237
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js3
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js7
-rw-r--r--app/assets/javascripts/lib/vue_resource.js.es62
-rw-r--r--app/assets/javascripts/line_highlighter.js15
-rw-r--r--app/assets/javascripts/logo.js5
-rw-r--r--app/assets/javascripts/member_expiration_date.js.es6 (renamed from app/assets/javascripts/member_expiration_date.js)17
-rw-r--r--app/assets/javascripts/members.js.es665
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es616
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es66
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es615
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es626
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es66
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es643
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es618
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es64
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es64
-rw-r--r--app/assets/javascripts/merge_request.js30
-rw-r--r--app/assets/javascripts/merge_request_tabs.js441
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.es6365
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es6124
-rw-r--r--app/assets/javascripts/merge_request_widget/ci_bundle.js.es653
-rw-r--r--app/assets/javascripts/merged_buttons.js7
-rw-r--r--app/assets/javascripts/milestone.js6
-rw-r--r--app/assets/javascripts/milestone_select.js8
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es697
-rw-r--r--app/assets/javascripts/namespace_select.js15
-rw-r--r--app/assets/javascripts/network/branch_graph.js40
-rw-r--r--app/assets/javascripts/network/network.js6
-rw-r--r--app/assets/javascripts/network/network_bundle.js6
-rw-r--r--app/assets/javascripts/new_branch_form.js8
-rw-r--r--app/assets/javascripts/new_commit_form.js6
-rw-r--r--app/assets/javascripts/notes.js271
-rw-r--r--app/assets/javascripts/notifications_dropdown.js8
-rw-r--r--app/assets/javascripts/notifications_form.js6
-rw-r--r--app/assets/javascripts/pager.js64
-rw-r--r--app/assets/javascripts/pager.js.es673
-rw-r--r--app/assets/javascripts/pipelines.js.es634
-rw-r--r--app/assets/javascripts/preview_markdown.js131
-rw-r--r--app/assets/javascripts/profile/gl_crop.js.es610
-rw-r--r--app/assets/javascripts/profile/profile.js.es616
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js2
-rw-r--r--app/assets/javascripts/project.js16
-rw-r--r--app/assets/javascripts/project_avatar.js4
-rw-r--r--app/assets/javascripts/project_find_file.js12
-rw-r--r--app/assets/javascripts/project_fork.js4
-rw-r--r--app/assets/javascripts/project_import.js6
-rw-r--r--app/assets/javascripts/project_label_subscription.js.es653
-rw-r--r--app/assets/javascripts/project_new.js36
-rw-r--r--app/assets/javascripts/project_select.js9
-rw-r--r--app/assets/javascripts/project_show.js5
-rw-r--r--app/assets/javascripts/project_variables.js.es643
-rw-r--r--app/assets/javascripts/projects_list.js8
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es68
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js.es610
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es65
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js.es612
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es610
-rw-r--r--app/assets/javascripts/protected_branches/protected_branches_bundle.js1
-rw-r--r--app/assets/javascripts/render_gfm.js15
-rw-r--r--app/assets/javascripts/render_math.js54
-rw-r--r--app/assets/javascripts/right_sidebar.js10
-rw-r--r--app/assets/javascripts/search.js12
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es631
-rw-r--r--app/assets/javascripts/shortcuts.js12
-rw-r--r--app/assets/javascripts/shortcuts_blob.js6
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js6
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js6
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js8
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js6
-rw-r--r--app/assets/javascripts/shortcuts_network.js6
-rw-r--r--app/assets/javascripts/sidebar.js.es65
-rw-r--r--app/assets/javascripts/signin_tabs_memoizer.js.es649
-rw-r--r--app/assets/javascripts/single_file_diff.js40
-rw-r--r--app/assets/javascripts/smart_interval.js.es6157
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js7
-rw-r--r--app/assets/javascripts/snippets_list.js.es65
-rw-r--r--app/assets/javascripts/star.js6
-rw-r--r--app/assets/javascripts/subbable_resource.js.es651
-rw-r--r--app/assets/javascripts/subscription.js52
-rw-r--r--app/assets/javascripts/subscription.js.es650
-rw-r--r--app/assets/javascripts/subscription_select.js4
-rw-r--r--app/assets/javascripts/syntax_highlight.js9
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es612
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es65
-rw-r--r--app/assets/javascripts/terminal/terminal.js.es662
-rw-r--r--app/assets/javascripts/terminal/terminal_bundle.js.es67
-rw-r--r--app/assets/javascripts/todos.js.es610
-rw-r--r--app/assets/javascripts/tree.js5
-rw-r--r--app/assets/javascripts/u2f/authenticate.js.es6 (renamed from app/assets/javascripts/u2f/authenticate.js)50
-rw-r--r--app/assets/javascripts/u2f/error.js25
-rw-r--r--app/assets/javascripts/u2f/register.js15
-rw-r--r--app/assets/javascripts/u2f/util.js4
-rw-r--r--app/assets/javascripts/user.js.es66
-rw-r--r--app/assets/javascripts/user_tabs.js.es69
-rw-r--r--app/assets/javascripts/username_validator.js.es65
-rw-r--r--app/assets/javascripts/users/calendar.js18
-rw-r--r--app/assets/javascripts/users/users_bundle.js2
-rw-r--r--app/assets/javascripts/users_select.js16
-rw-r--r--app/assets/javascripts/visibility_select.js.es627
-rw-r--r--app/assets/javascripts/vue_common_component/commit.js.es6163
-rw-r--r--app/assets/javascripts/vue_pagination/index.js.es6148
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js.es642
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es699
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es663
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es6131
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es6102
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stages.js.es621
-rw-r--r--app/assets/javascripts/vue_pipelines_index/status.js.es634
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es669
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js.es673
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js.es618
-rw-r--r--app/assets/javascripts/wikis.js38
-rw-r--r--app/assets/javascripts/wikis.js.es673
-rw-r--r--app/assets/javascripts/zen_mode.js7
-rw-r--r--app/assets/stylesheets/framework.scss11
-rw-r--r--app/assets/stylesheets/framework/animations.scss74
-rw-r--r--app/assets/stylesheets/framework/asciidoctor.scss27
-rw-r--r--app/assets/stylesheets/framework/avatar.scss16
-rw-r--r--app/assets/stylesheets/framework/awards.scss (renamed from app/assets/stylesheets/pages/awards.scss)29
-rw-r--r--app/assets/stylesheets/framework/badges.scss6
-rw-r--r--app/assets/stylesheets/framework/blank.scss4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss60
-rw-r--r--app/assets/stylesheets/framework/broadcast-messages.scss21
-rw-r--r--app/assets/stylesheets/framework/buttons.scss79
-rw-r--r--app/assets/stylesheets/framework/calendar.scss6
-rw-r--r--app/assets/stylesheets/framework/callout.scss26
-rw-r--r--app/assets/stylesheets/framework/common.scss117
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss123
-rw-r--r--app/assets/stylesheets/framework/emojis.scss (renamed from app/assets/stylesheets/pages/emojis.scss)2
-rw-r--r--app/assets/stylesheets/framework/files.scss71
-rw-r--r--app/assets/stylesheets/framework/filters.scss115
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/fonts.scss45
-rw-r--r--app/assets/stylesheets/framework/forms.scss61
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss48
-rw-r--r--app/assets/stylesheets/framework/header.scss54
-rw-r--r--app/assets/stylesheets/framework/highlight.scss4
-rw-r--r--app/assets/stylesheets/framework/icons.scss60
-rw-r--r--app/assets/stylesheets/framework/images.scss (renamed from app/assets/stylesheets/pages/appearances.scss)2
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss8
-rw-r--r--app/assets/stylesheets/framework/jquery.scss16
-rw-r--r--app/assets/stylesheets/framework/layout.scss58
-rw-r--r--app/assets/stylesheets/framework/lists.scss116
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss44
-rw-r--r--app/assets/stylesheets/framework/mixins.scss8
-rw-r--r--app/assets/stylesheets/framework/mobile.scss18
-rw-r--r--app/assets/stylesheets/framework/nav.scss172
-rw-r--r--app/assets/stylesheets/framework/page-header.scss62
-rw-r--r--app/assets/stylesheets/framework/pagination.scss4
-rw-r--r--app/assets/stylesheets/framework/panels.scss22
-rw-r--r--app/assets/stylesheets/framework/selects.scss20
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss32
-rw-r--r--app/assets/stylesheets/framework/snippets.scss (renamed from app/assets/stylesheets/pages/snippets.scss)26
-rw-r--r--app/assets/stylesheets/framework/tables.scss25
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss12
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss37
-rw-r--r--app/assets/stylesheets/framework/typography.scss33
-rw-r--r--app/assets/stylesheets/framework/variables.scss441
-rw-r--r--app/assets/stylesheets/framework/wells.scss58
-rw-r--r--app/assets/stylesheets/framework/zen.scss10
-rw-r--r--app/assets/stylesheets/highlight/dark.scss230
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss225
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss243
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss243
-rw-r--r--app/assets/stylesheets/highlight/white.scss187
-rw-r--r--app/assets/stylesheets/mailers/devise.scss4
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss207
-rw-r--r--app/assets/stylesheets/mailers/repository_push_email.scss143
-rw-r--r--app/assets/stylesheets/notify.scss18
-rw-r--r--app/assets/stylesheets/pages/admin.scss162
-rw-r--r--app/assets/stylesheets/pages/boards.scss45
-rw-r--r--app/assets/stylesheets/pages/branches.scss55
-rw-r--r--app/assets/stylesheets/pages/builds.scss172
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss4
-rw-r--r--app/assets/stylesheets/pages/commit.scss252
-rw-r--r--app/assets/stylesheets/pages/commits.scss199
-rw-r--r--app/assets/stylesheets/pages/confirmation.scss32
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss374
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss43
-rw-r--r--app/assets/stylesheets/pages/deploy_keys.scss13
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss9
-rw-r--r--app/assets/stylesheets/pages/diff.scss69
-rw-r--r--app/assets/stylesheets/pages/editor.scss63
-rw-r--r--app/assets/stylesheets/pages/environments.scss85
-rw-r--r--app/assets/stylesheets/pages/errors.scss16
-rw-r--r--app/assets/stylesheets/pages/events.scss13
-rw-r--r--app/assets/stylesheets/pages/explore.scss8
-rw-r--r--app/assets/stylesheets/pages/graph.scss13
-rw-r--r--app/assets/stylesheets/pages/groups.scss33
-rw-r--r--app/assets/stylesheets/pages/help.scss10
-rw-r--r--app/assets/stylesheets/pages/icons.scss12
-rw-r--r--app/assets/stylesheets/pages/issuable.scss217
-rw-r--r--app/assets/stylesheets/pages/issues.scss37
-rw-r--r--app/assets/stylesheets/pages/labels.scss25
-rw-r--r--app/assets/stylesheets/pages/lint.scss18
-rw-r--r--app/assets/stylesheets/pages/login.scss43
-rw-r--r--app/assets/stylesheets/pages/members.scss41
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss10
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss89
-rw-r--r--app/assets/stylesheets/pages/milestone.scss118
-rw-r--r--app/assets/stylesheets/pages/note_form.scss30
-rw-r--r--app/assets/stylesheets/pages/notes.scss225
-rw-r--r--app/assets/stylesheets/pages/notifications.scss18
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss831
-rw-r--r--app/assets/stylesheets/pages/profile.scss37
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss241
-rw-r--r--app/assets/stylesheets/pages/runners.scss12
-rw-r--r--app/assets/stylesheets/pages/search.scss31
-rw-r--r--app/assets/stylesheets/pages/settings.scss6
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss16
-rw-r--r--app/assets/stylesheets/pages/status.scss123
-rw-r--r--app/assets/stylesheets/pages/tags.scss7
-rw-r--r--app/assets/stylesheets/pages/todos.scss10
-rw-r--r--app/assets/stylesheets/pages/tree.scss40
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss4
-rw-r--r--app/assets/stylesheets/pages/votes.scss4
-rw-r--r--app/assets/stylesheets/pages/wiki.scss125
-rw-r--r--app/assets/stylesheets/pages/xterm.scss774
-rw-r--r--app/controllers/admin/application_settings_controller.rb101
-rw-r--r--app/controllers/admin/applications_controller.rb5
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb4
-rw-r--r--app/controllers/admin/groups_controller.rb17
-rw-r--r--app/controllers/admin/projects_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb44
-rw-r--r--app/controllers/application_controller.rb18
-rw-r--r--app/controllers/autocomplete_controller.rb12
-rw-r--r--app/controllers/concerns/creates_commit.rb6
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb11
-rw-r--r--app/controllers/concerns/diff_for_path.rb2
-rw-r--r--app/controllers/concerns/global_milestones.rb20
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/issuable_collections.rb4
-rw-r--r--app/controllers/concerns/issues_action.rb1
-rw-r--r--app/controllers/concerns/lfs_request.rb (renamed from app/helpers/lfs_helper.rb)72
-rw-r--r--app/controllers/concerns/merge_requests_action.rb8
-rw-r--r--app/controllers/concerns/oauth_applications.rb19
-rw-r--r--app/controllers/concerns/service_params.rb79
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb5
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb6
-rw-r--r--app/controllers/concerns/workhorse_request.rb13
-rw-r--r--app/controllers/confirmations_controller.rb8
-rw-r--r--app/controllers/dashboard/milestones_controller.rb13
-rw-r--r--app/controllers/dashboard/todos_controller.rb3
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb12
-rw-r--r--app/controllers/groups/labels_controller.rb7
-rw-r--r--app/controllers/groups/milestones_controller.rb15
-rw-r--r--app/controllers/groups_controller.rb14
-rw-r--r--app/controllers/help_controller.rb8
-rw-r--r--app/controllers/import/bitbucket_controller.rb83
-rw-r--r--app/controllers/import/gitea_controller.rb45
-rw-r--r--app/controllers/import/github_controller.rb95
-rw-r--r--app/controllers/jwt_controller.rb2
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/controllers/profiles/avatars_controller.rb1
-rw-r--r--app/controllers/profiles/chat_names_controller.rb64
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb12
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb8
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb48
-rw-r--r--app/controllers/projects/avatars_controller.rb1
-rw-r--r--app/controllers/projects/blame_controller.rb3
-rw-r--r--app/controllers/projects/blob_controller.rb20
-rw-r--r--app/controllers/projects/branches_controller.rb11
-rw-r--r--app/controllers/projects/builds_controller.rb2
-rw-r--r--app/controllers/projects/commit_controller.rb52
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects/compare_controller.rb13
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb61
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb49
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb4
-rw-r--r--app/controllers/projects/discussions_controller.rb6
-rw-r--r--app/controllers/projects/environments_controller.rb44
-rw-r--r--app/controllers/projects/forks_controller.rb1
-rw-r--r--app/controllers/projects/git_http_client_controller.rb16
-rw-r--r--app/controllers/projects/git_http_controller.rb12
-rw-r--r--app/controllers/projects/group_links_controller.rb9
-rw-r--r--app/controllers/projects/hooks_controller.rb14
-rw-r--r--app/controllers/projects/issues_controller.rb32
-rw-r--r--app/controllers/projects/labels_controller.rb7
-rw-r--r--app/controllers/projects/lfs_api_controller.rb21
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb7
-rw-r--r--app/controllers/projects/mattermosts_controller.rb43
-rw-r--r--app/controllers/projects/merge_requests_controller.rb83
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/controllers/projects/notes_controller.rb31
-rw-r--r--app/controllers/projects/pipelines_controller.rb52
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb28
-rw-r--r--app/controllers/projects/releases_controller.rb9
-rw-r--r--app/controllers/projects/services_controller.rb9
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb18
-rw-r--r--app/controllers/projects/settings/members_controller.rb55
-rw-r--r--app/controllers/projects/snippets_controller.rb11
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects/todos_controller.rb10
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb44
-rw-r--r--app/controllers/registrations_controller.rb23
-rw-r--r--app/controllers/search_controller.rb14
-rw-r--r--app/controllers/sent_notifications_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb10
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/issuable_finder.rb115
-rw-r--r--app/finders/issues_finder.rb18
-rw-r--r--app/finders/labels_finder.rb47
-rw-r--r--app/finders/members_finder.rb13
-rw-r--r--app/finders/merge_requests_finder.rb1
-rw-r--r--app/finders/notes_finder.rb113
-rw-r--r--app/finders/snippets_finder.rb47
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/application_settings_helper.rb22
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb6
-rw-r--r--app/helpers/builds_helper.rb2
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb25
-rw-r--r--app/helpers/commits_helper.rb65
-rw-r--r--app/helpers/diff_helper.rb13
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/environment_helper.rb31
-rw-r--r--app/helpers/environments_helper.rb7
-rw-r--r--app/helpers/events_helper.rb10
-rw-r--r--app/helpers/form_helper.rb12
-rw-r--r--app/helpers/gitlab_markdown_helper.rb44
-rw-r--r--app/helpers/gitlab_routing_helper.rb18
-rw-r--r--app/helpers/groups_helper.rb23
-rw-r--r--app/helpers/import_helper.rb10
-rw-r--r--app/helpers/issuables_helper.rb51
-rw-r--r--app/helpers/issues_helper.rb12
-rw-r--r--app/helpers/labels_helper.rb39
-rw-r--r--app/helpers/mattermost_helper.rb9
-rw-r--r--app/helpers/members_helper.rb8
-rw-r--r--app/helpers/merge_requests_helper.rb12
-rw-r--r--app/helpers/milestones_helper.rb24
-rw-r--r--app/helpers/nav_helper.rb17
-rw-r--r--app/helpers/preferences_helper.rb6
-rw-r--r--app/helpers/projects_helper.rb124
-rw-r--r--app/helpers/search_helper.rb33
-rw-r--r--app/helpers/services_helper.rb20
-rw-r--r--app/helpers/sidekiq_helper.rb10
-rw-r--r--app/helpers/snippets_helper.rb11
-rw-r--r--app/helpers/sorting_helper.rb70
-rw-r--r--app/helpers/storage_helper.rb7
-rw-r--r--app/helpers/tab_helper.rb6
-rw-r--r--app/helpers/todos_helper.rb5
-rw-r--r--app/helpers/triggers_helper.rb12
-rw-r--r--app/mailers/emails/notes.rb2
-rw-r--r--app/mailers/notify.rb20
-rw-r--r--app/models/application_setting.rb27
-rw-r--r--app/models/chat_name.rb12
-rw-r--r--app/models/ci/build.rb110
-rw-r--r--app/models/ci/pipeline.rb97
-rw-r--r--app/models/ci/runner.rb33
-rw-r--r--app/models/ci/stage.rb56
-rw-r--r--app/models/ci/variable.rb4
-rw-r--r--app/models/commit.rb102
-rw-r--r--app/models/commit_range.rb19
-rw-r--r--app/models/commit_status.rb47
-rw-r--r--app/models/concerns/has_status.rb11
-rw-r--r--app/models/concerns/issuable.rb21
-rw-r--r--app/models/concerns/mentionable.rb2
-rw-r--r--app/models/concerns/milestoneish.rb71
-rw-r--r--app/models/concerns/presentable.rb7
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/concerns/protected_branch_access.rb9
-rw-r--r--app/models/concerns/reactive_caching.rb118
-rw-r--r--app/models/concerns/reactive_service.rb10
-rw-r--r--app/models/concerns/referable.rb15
-rw-r--r--app/models/concerns/routable.rb71
-rw-r--r--app/models/concerns/select_for_project_authorization.rb9
-rw-r--r--app/models/concerns/subscribable.rb64
-rw-r--r--app/models/concerns/time_trackable.rb72
-rw-r--r--app/models/concerns/token_authenticatable.rb4
-rw-r--r--app/models/concerns/valid_attribute.rb10
-rw-r--r--app/models/cycle_analytics.rb99
-rw-r--r--app/models/cycle_analytics/summary.rb42
-rw-r--r--app/models/dashboard_milestone.rb5
-rw-r--r--app/models/deploy_key.rb14
-rw-r--r--app/models/discussion.rb16
-rw-r--r--app/models/environment.rb89
-rw-r--r--app/models/event.rb16
-rw-r--r--app/models/external_issue.rb2
-rw-r--r--app/models/forked_project_link.rb4
-rw-r--r--app/models/generic_commit_status.rb10
-rw-r--r--app/models/global_milestone.rb62
-rw-r--r--app/models/group.rb43
-rw-r--r--app/models/group_label.rb4
-rw-r--r--app/models/group_milestone.rb19
-rw-r--r--app/models/issue.rb79
-rw-r--r--app/models/key.rb42
-rw-r--r--app/models/label.rb18
-rw-r--r--app/models/lfs_objects_project.rb9
-rw-r--r--app/models/member.rb53
-rw-r--r--app/models/merge_request.rb138
-rw-r--r--app/models/merge_request/metrics.rb1
-rw-r--r--app/models/merge_request_diff.rb38
-rw-r--r--app/models/milestone.rb44
-rw-r--r--app/models/namespace.rb91
-rw-r--r--app/models/network/graph.rb4
-rw-r--r--app/models/note.rb36
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/personal_access_token.rb2
-rw-r--r--app/models/project.rb321
-rw-r--r--app/models/project_authorization.rb21
-rw-r--r--app/models/project_feature.rb10
-rw-r--r--app/models/project_group_link.rb7
-rw-r--r--app/models/project_label.rb4
-rw-r--r--app/models/project_services/asana_service.rb4
-rw-r--r--app/models/project_services/assembla_service.rb4
-rw-r--r--app/models/project_services/bamboo_service.rb49
-rw-r--r--app/models/project_services/bugzilla_service.rb2
-rw-r--r--app/models/project_services/buildkite_service.rb32
-rw-r--r--app/models/project_services/builds_email_service.rb4
-rw-r--r--app/models/project_services/campfire_service.rb4
-rw-r--r--app/models/project_services/chat_message/base_message.rb (renamed from app/models/project_services/slack_service/base_message.rb)2
-rw-r--r--app/models/project_services/chat_message/build_message.rb (renamed from app/models/project_services/slack_service/build_message.rb)2
-rw-r--r--app/models/project_services/chat_message/issue_message.rb (renamed from app/models/project_services/slack_service/issue_message.rb)2
-rw-r--r--app/models/project_services/chat_message/merge_message.rb (renamed from app/models/project_services/slack_service/merge_message.rb)2
-rw-r--r--app/models/project_services/chat_message/note_message.rb (renamed from app/models/project_services/slack_service/note_message.rb)14
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb (renamed from app/models/project_services/slack_service/pipeline_message.rb)9
-rw-r--r--app/models/project_services/chat_message/push_message.rb (renamed from app/models/project_services/slack_service/push_message.rb)2
-rw-r--r--app/models/project_services/chat_message/wiki_page_message.rb (renamed from app/models/project_services/slack_service/wiki_page_message.rb)2
-rw-r--r--app/models/project_services/chat_notification_service.rb149
-rw-r--r--app/models/project_services/chat_slash_commands_service.rb56
-rw-r--r--app/models/project_services/ci_service.rb29
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/deployment_service.rb33
-rw-r--r--app/models/project_services/drone_ci_service.rb83
-rw-r--r--app/models/project_services/emails_on_push_service.rb22
-rw-r--r--app/models/project_services/external_wiki_service.rb6
-rw-r--r--app/models/project_services/flowdock_service.rb4
-rw-r--r--app/models/project_services/gemnasium_service.rb4
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb10
-rw-r--r--app/models/project_services/irker_service.rb7
-rw-r--r--app/models/project_services/issue_tracker_service.rb6
-rw-r--r--app/models/project_services/jira_service.rb238
-rw-r--r--app/models/project_services/kubernetes_service.rb173
-rw-r--r--app/models/project_services/mattermost_service.rb41
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb51
-rw-r--r--app/models/project_services/pipelines_email_service.rb4
-rw-r--r--app/models/project_services/pivotaltracker_service.rb4
-rw-r--r--app/models/project_services/pushover_service.rb4
-rw-r--r--app/models/project_services/redmine_service.rb2
-rw-r--r--app/models/project_services/slack_service.rb176
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb28
-rw-r--r--app/models/project_services/teamcity_service.rb79
-rw-r--r--app/models/project_statistics.rb44
-rw-r--r--app/models/project_team.rb133
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/protected_branch/merge_access_level.rb9
-rw-r--r--app/models/protected_branch/push_access_level.rb6
-rw-r--r--app/models/repository.rb489
-rw-r--r--app/models/route.rb22
-rw-r--r--app/models/service.rb26
-rw-r--r--app/models/snippet.rb19
-rw-r--r--app/models/subscription.rb7
-rw-r--r--app/models/timelog.rb6
-rw-r--r--app/models/todo.rb8
-rw-r--r--app/models/tree.rb26
-rw-r--r--app/models/user.rb197
-rw-r--r--app/policies/base_policy.rb4
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/policies/group_member_policy.rb6
-rw-r--r--app/policies/group_policy.rb8
-rw-r--r--app/policies/note_policy.rb2
-rw-r--r--app/policies/personal_snippet_policy.rb5
-rw-r--r--app/policies/project_policy.rb36
-rw-r--r--app/presenters/README.md154
-rw-r--r--app/presenters/ci/build_presenter.rb15
-rw-r--r--app/serializers/analytics_build_entity.rb40
-rw-r--r--app/serializers/analytics_build_serializer.rb3
-rw-r--r--app/serializers/analytics_commit_entity.rb13
-rw-r--r--app/serializers/analytics_commit_serializer.rb3
-rw-r--r--app/serializers/analytics_generic_serializer.rb7
-rw-r--r--app/serializers/analytics_issue_entity.rb29
-rw-r--r--app/serializers/analytics_issue_serializer.rb3
-rw-r--r--app/serializers/analytics_merge_request_entity.rb7
-rw-r--r--app/serializers/analytics_merge_request_serializer.rb3
-rw-r--r--app/serializers/analytics_stage_entity.rb10
-rw-r--r--app/serializers/analytics_stage_serializer.rb3
-rw-r--r--app/serializers/analytics_summary_entity.rb7
-rw-r--r--app/serializers/analytics_summary_serializer.rb3
-rw-r--r--app/serializers/build_action_entity.rb14
-rw-r--r--app/serializers/build_artifact_entity.rb14
-rw-r--r--app/serializers/build_entity.rb19
-rw-r--r--app/serializers/commit_entity.rb15
-rw-r--r--app/serializers/deployment_entity.rb4
-rw-r--r--app/serializers/entity_date_helper.rb37
-rw-r--r--app/serializers/environment_entity.rb19
-rw-r--r--app/serializers/issuable_entity.rb20
-rw-r--r--app/serializers/issue_entity.rb9
-rw-r--r--app/serializers/issue_serializer.rb3
-rw-r--r--app/serializers/label_entity.rb11
-rw-r--r--app/serializers/merge_request_entity.rb14
-rw-r--r--app/serializers/merge_request_serializer.rb3
-rw-r--r--app/serializers/pipeline_entity.rb85
-rw-r--r--app/serializers/pipeline_serializer.rb40
-rw-r--r--app/serializers/request_aware_entity.rb5
-rw-r--r--app/serializers/stage_entity.rb38
-rw-r--r--app/serializers/status_entity.rb8
-rw-r--r--app/services/access_token_validation_service.rb32
-rw-r--r--app/services/after_branch_delete_service.rb21
-rw-r--r--app/services/chat_names/authorize_user_service.rb38
-rw-r--r--app/services/chat_names/find_user_service.rb26
-rw-r--r--app/services/ci/create_pipeline_builds_service.rb15
-rw-r--r--app/services/ci/create_pipeline_service.rb12
-rw-r--r--app/services/ci/image_for_build_service.rb8
-rw-r--r--app/services/ci/process_pipeline_service.rb26
-rw-r--r--app/services/ci/stop_environments_service.rb29
-rw-r--r--app/services/ci/update_build_queue_service.rb19
-rw-r--r--app/services/commits/change_service.rb4
-rw-r--r--app/services/create_branch_service.rb2
-rw-r--r--app/services/create_deployment_service.rb2
-rw-r--r--app/services/create_release_service.rb2
-rw-r--r--app/services/create_tag_service.rb2
-rw-r--r--app/services/delete_branch_service.rb2
-rw-r--r--app/services/delete_merged_branches_service.rb16
-rw-r--r--app/services/delete_tag_service.rb2
-rw-r--r--app/services/destroy_group_service.rb14
-rw-r--r--app/services/discussions/base_service.rb4
-rw-r--r--app/services/discussions/resolve_service.rb24
-rw-r--r--app/services/files/create_dir_service.rb2
-rw-r--r--app/services/files/create_service.rb2
-rw-r--r--app/services/files/delete_service.rb2
-rw-r--r--app/services/files/multi_service.rb2
-rw-r--r--app/services/files/update_service.rb2
-rw-r--r--app/services/git_hooks_service.rb6
-rw-r--r--app/services/git_push_service.rb66
-rw-r--r--app/services/git_tag_push_service.rb2
-rw-r--r--app/services/groups/create_service.rb7
-rw-r--r--app/services/groups/update_service.rb10
-rw-r--r--app/services/issuable_base_service.rb81
-rw-r--r--app/services/issues/base_service.rb12
-rw-r--r--app/services/issues/build_service.rb50
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/issues/create_service.rb14
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/labels/find_or_create_service.rb7
-rw-r--r--app/services/merge_requests/add_todo_when_build_fails_service.rb7
-rw-r--r--app/services/merge_requests/base_service.rb24
-rw-r--r--app/services/merge_requests/build_service.rb8
-rw-r--r--app/services/merge_requests/merge_when_pipeline_succeeds_service.rb (renamed from app/services/merge_requests/merge_when_build_succeeds_service.rb)2
-rw-r--r--app/services/merge_requests/refresh_service.rb47
-rw-r--r--app/services/merge_requests/update_service.rb21
-rw-r--r--app/services/notes/create_service.rb18
-rw-r--r--app/services/notes/delete_service.rb1
-rw-r--r--app/services/notes/slash_commands_service.rb4
-rw-r--r--app/services/notes/update_service.rb1
-rw-r--r--app/services/notification_service.rb34
-rw-r--r--app/services/oauth2/access_token_validation_service.rb42
-rw-r--r--app/services/projects/create_service.rb11
-rw-r--r--app/services/projects/import_service.rb16
-rw-r--r--app/services/projects/participants_service.rb9
-rw-r--r--app/services/projects/transfer_service.rb3
-rw-r--r--app/services/projects/update_service.rb8
-rw-r--r--app/services/slash_commands/interpret_service.rb64
-rw-r--r--app/services/system_note_service.rb155
-rw-r--r--app/services/todo_service.rb30
-rw-r--r--app/services/update_release_service.rb2
-rw-r--r--app/services/user_project_access_changed_service.rb9
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb127
-rw-r--r--app/uploaders/artifact_uploader.rb10
-rw-r--r--app/uploaders/attachment_uploader.rb2
-rw-r--r--app/uploaders/avatar_uploader.rb19
-rw-r--r--app/uploaders/file_uploader.rb2
-rw-r--r--app/uploaders/gitlab_uploader.rb11
-rw-r--r--app/uploaders/lfs_object_uploader.rb10
-rw-r--r--app/uploaders/uploader_helper.rb2
-rw-r--r--app/validators/namespace_validator.rb22
-rw-r--r--app/validators/project_path_validator.rb37
-rw-r--r--app/views/abuse_report_mailer/notify.html.haml6
-rw-r--r--app/views/admin/abuse_reports/index.html.haml10
-rw-r--r--app/views/admin/application_settings/_form.html.haml68
-rw-r--r--app/views/admin/applications/_delete_form.html.haml2
-rw-r--r--app/views/admin/applications/_form.html.haml6
-rw-r--r--app/views/admin/applications/index.html.haml2
-rw-r--r--app/views/admin/applications/show.html.haml6
-rw-r--r--app/views/admin/background_jobs/show.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml2
-rw-r--r--app/views/admin/builds/index.html.haml2
-rw-r--r--app/views/admin/dashboard/_head.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml14
-rw-r--r--app/views/admin/deploy_keys/index.html.haml57
-rw-r--r--app/views/admin/deploy_keys/new.html.haml8
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/_group.html.haml9
-rw-r--r--app/views/admin/groups/edit.html.haml2
-rw-r--r--app/views/admin/groups/index.html.haml2
-rw-r--r--app/views/admin/groups/new.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml28
-rw-r--r--app/views/admin/hooks/index.html.haml5
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/logs/show.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml16
-rw-r--r--app/views/admin/projects/show.html.haml13
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/admin/services/_form.html.haml7
-rw-r--r--app/views/admin/users/_head.html.haml4
-rw-r--r--app/views/admin/users/_user.html.haml10
-rw-r--r--app/views/admin/users/groups.html.haml20
-rw-r--r--app/views/admin/users/index.html.haml2
-rw-r--r--app/views/admin/users/projects.html.haml18
-rw-r--r--app/views/admin/users/show.html.haml2
-rw-r--r--app/views/award_emoji/_awards_block.html.haml4
-rw-r--r--app/views/ci/lints/_create.html.haml2
-rw-r--r--app/views/ci/lints/show.html.haml31
-rw-r--r--app/views/ci/status/_badge.html.haml11
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml19
-rw-r--r--app/views/ci/status/_graph_badge.html.haml20
-rw-r--r--app/views/dashboard/_activity_head.html.haml4
-rw-r--r--app/views/dashboard/_projects_head.html.haml6
-rw-r--r--app/views/dashboard/_snippets_head.html.haml20
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml25
-rw-r--r--app/views/dashboard/snippets/index.html.haml42
-rw-r--r--app/views/dashboard/todos/_todo.html.haml4
-rw-r--r--app/views/dashboard/todos/index.html.haml16
-rw-r--r--app/views/devise/confirmations/almost_there.haml7
-rw-r--r--app/views/devise/sessions/_new_base.html.haml10
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml4
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml4
-rw-r--r--app/views/devise/sessions/new.html.haml1
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_signin_box.html.haml8
-rw-r--r--app/views/devise/shared/_signup_box.html.haml13
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml5
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml8
-rw-r--r--app/views/discussions/_diff_discussion.html.haml2
-rw-r--r--app/views/discussions/_discussion.html.haml6
-rw-r--r--app/views/discussions/_jump_to_next.html.haml2
-rw-r--r--app/views/discussions/_parallel_diff_discussion.html.haml6
-rw-r--r--app/views/doorkeeper/applications/_delete_form.html.haml2
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml4
-rw-r--r--app/views/doorkeeper/applications/index.html.haml6
-rw-r--r--app/views/doorkeeper/applications/show.html.haml5
-rw-r--r--app/views/doorkeeper/authorizations/error.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/show.html.haml2
-rw-r--r--app/views/doorkeeper/authorized_applications/_delete_form.html.haml4
-rw-r--r--app/views/doorkeeper/authorized_applications/index.html.haml2
-rw-r--r--app/views/emojis/index.html.haml2
-rw-r--r--app/views/errors/access_denied.html.haml15
-rw-r--r--app/views/errors/encoding.html.haml13
-rw-r--r--app/views/errors/git_not_found.html.haml16
-rw-r--r--app/views/errors/not_found.html.haml13
-rw-r--r--app/views/errors/omniauth_error.html.haml15
-rw-r--r--app/views/events/_event.html.haml19
-rw-r--r--app/views/events/_event_issue.atom.haml2
-rw-r--r--app/views/events/_event_merge_request.atom.haml2
-rw-r--r--app/views/events/_event_note.atom.haml2
-rw-r--r--app/views/events/_event_push.atom.haml2
-rw-r--r--app/views/events/event/_common.html.haml2
-rw-r--r--app/views/events/event/_created_project.html.haml2
-rw-r--r--app/views/events/event/_push.html.haml6
-rw-r--r--app/views/explore/_head.html.haml4
-rw-r--r--app/views/explore/groups/index.html.haml4
-rw-r--r--app/views/explore/projects/_filter.html.haml8
-rw-r--r--app/views/explore/snippets/index.html.haml10
-rw-r--r--app/views/groups/_group_lfs_settings.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml1
-rw-r--r--app/views/groups/group_members/update.js.haml1
-rw-r--r--app/views/groups/issues.html.haml37
-rw-r--r--app/views/groups/merge_requests.html.haml5
-rw-r--r--app/views/groups/milestones/new.html.haml15
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/projects.html.haml4
-rw-r--r--app/views/groups/show.html.haml43
-rw-r--r--app/views/help/_shortcuts.html.haml12
-rw-r--r--app/views/help/show.html.haml2
-rw-r--r--app/views/help/ui.html.haml124
-rw-r--r--app/views/import/_githubish_status.html.haml61
-rw-r--r--app/views/import/bitbucket/status.html.haml47
-rw-r--r--app/views/import/fogbugz/status.html.haml4
-rw-r--r--app/views/import/gitea/new.html.haml23
-rw-r--r--app/views/import/gitea/status.html.haml7
-rw-r--r--app/views/import/github/status.html.haml64
-rw-r--r--app/views/import/gitlab/status.html.haml4
-rw-r--r--app/views/import/google_code/new.html.haml2
-rw-r--r--app/views/import/google_code/status.html.haml6
-rw-r--r--app/views/invites/show.html.haml13
-rw-r--r--app/views/kaminari/gitlab/_next_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_paginator.html.haml3
-rw-r--r--app/views/kaminari/gitlab/_prev_page.html.haml4
-rw-r--r--app/views/layouts/_head.html.haml42
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml14
-rw-r--r--app/views/layouts/_page.html.haml9
-rw-r--r--app/views/layouts/_search.html.haml4
-rw-r--r--app/views/layouts/_user_styles.html.haml24
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/devise_empty.html.haml2
-rw-r--r--app/views/layouts/devise_mailer.html.haml4
-rw-r--r--app/views/layouts/errors.html.haml67
-rw-r--r--app/views/layouts/header/_default.html.haml7
-rw-r--r--app/views/layouts/nav/_admin_settings.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml9
-rw-r--r--app/views/layouts/nav/_group.html.haml2
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml14
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml23
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml19
-rw-r--r--app/views/layouts/notify.html.haml8
-rw-r--r--app/views/layouts/profile.html.haml4
-rw-r--r--app/views/notify/_note_message.text.erb5
-rw-r--r--app/views/notify/_note_mr_or_commit_email.html.haml18
-rw-r--r--app/views/notify/_note_mr_or_commit_email.text.erb8
-rw-r--r--app/views/notify/_simple_diff.text.erb3
-rw-r--r--app/views/notify/build_fail_email.html.haml2
-rw-r--r--app/views/notify/build_success_email.html.haml2
-rw-r--r--app/views/notify/links/ci/builds/_build.html.haml2
-rw-r--r--app/views/notify/links/ci/builds/_build.text.erb1
-rw-r--r--app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml1
-rw-r--r--app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb1
-rw-r--r--app/views/notify/new_issue_email.html.haml2
-rw-r--r--app/views/notify/new_mention_in_issue_email.html.haml2
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.html.haml2
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/note_commit_email.html.haml4
-rw-r--r--app/views/notify/note_commit_email.text.erb11
-rw-r--r--app/views/notify/note_merge_request_email.html.haml9
-rw-r--r--app/views/notify/note_merge_request_email.text.erb11
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml142
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb4
-rw-r--r--app/views/notify/pipeline_success_email.html.haml114
-rw-r--r--app/views/notify/pipeline_success_email.text.erb2
-rw-r--r--app/views/notify/project_was_not_exported_email.text.haml2
-rw-r--r--app/views/notify/repository_push_email.html.haml13
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml27
-rw-r--r--app/views/profiles/chat_names/index.html.haml30
-rw-r--r--app/views/profiles/chat_names/new.html.haml15
-rw-r--r--app/views/profiles/keys/_key.html.haml3
-rw-r--r--app/views/profiles/keys/_key_details.html.haml3
-rw-r--r--app/views/profiles/keys/index.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml4
-rw-r--r--app/views/profiles/personal_access_tokens/_form.html.haml21
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml19
-rw-r--r--app/views/profiles/preferences/show.html.haml4
-rw-r--r--app/views/profiles/show.html.haml13
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml2
-rw-r--r--app/views/profiles/update_username.js.haml3
-rw-r--r--app/views/projects/_activity.html.haml4
-rw-r--r--app/views/projects/_bitbucket_import_modal.html.haml6
-rw-r--r--app/views/projects/_customize_workflow.html.haml2
-rw-r--r--app/views/projects/_find_file_link.html.haml6
-rw-r--r--app/views/projects/_gitlab_import_modal.html.haml6
-rw-r--r--app/views/projects/_home_panel.html.haml18
-rw-r--r--app/views/projects/_last_commit.html.haml3
-rw-r--r--app/views/projects/_md_preview.html.haml20
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml15
-rw-r--r--app/views/projects/_merge_request_settings.html.haml26
-rw-r--r--app/views/projects/_readme.html.haml2
-rw-r--r--app/views/projects/_visibility_select.html.haml7
-rw-r--r--app/views/projects/_wiki.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml3
-rw-r--r--app/views/projects/blame/show.html.haml4
-rw-r--r--app/views/projects/blob/_blob.html.haml4
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/_image.html.haml4
-rw-r--r--app/views/projects/blob/_new_dir.html.haml2
-rw-r--r--app/views/projects/blob/_remove.html.haml2
-rw-r--r--app/views/projects/blob/_upload.html.haml4
-rw-r--r--app/views/projects/blob/diff.html.haml8
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/preview.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml2
-rw-r--r--app/views/projects/boards/_show.html.haml28
-rw-r--r--app/views/projects/boards/components/_blank_state.html.haml2
-rw-r--r--app/views/projects/boards/components/_board.html.haml112
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml44
-rw-r--r--app/views/projects/boards/components/_card.html.haml64
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml45
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml4
-rw-r--r--app/views/projects/boards/components/sidebar/_due_date.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_notifications.html.haml12
-rw-r--r--app/views/projects/boards/index.html.haml19
-rw-r--r--app/views/projects/boards/show.html.haml19
-rw-r--r--app/views/projects/branches/_branch.html.haml5
-rw-r--r--app/views/projects/branches/index.html.haml11
-rw-r--r--app/views/projects/branches/new.html.haml2
-rw-r--r--app/views/projects/builds/_header.html.haml9
-rw-r--r--app/views/projects/builds/_sidebar.html.haml19
-rw-r--r--app/views/projects/builds/_table.html.haml3
-rw-r--r--app/views/projects/builds/index.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml49
-rw-r--r--app/views/projects/buttons/_download.html.haml79
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml4
-rw-r--r--app/views/projects/buttons/_fork.html.haml2
-rw-r--r--app/views/projects/buttons/_koding.html.haml10
-rw-r--r--app/views/projects/buttons/_star.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml33
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml13
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml37
-rw-r--r--app/views/projects/commit/_builds.html.haml2
-rw-r--r--app/views/projects/commit/_change.html.haml17
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml6
-rw-r--r--app/views/projects/commit/_ci_stage.html.haml15
-rw-r--r--app/views/projects/commit/_commit_box.html.haml26
-rw-r--r--app/views/projects/commit/_pipeline.html.haml23
-rw-r--r--app/views/projects/commit/_pipeline_stage.html.haml14
-rw-r--r--app/views/projects/commit/_pipeline_status_group.html.haml13
-rw-r--r--app/views/projects/commit/_pipelines_list.haml24
-rw-r--r--app/views/projects/commit/builds.html.haml9
-rw-r--r--app/views/projects/commit/pipelines.html.haml2
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml47
-rw-r--r--app/views/projects/commits/_commit_list.html.haml4
-rw-r--r--app/views/projects/commits/_commits.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml4
-rw-r--r--app/views/projects/compare/_form.html.haml10
-rw-r--r--app/views/projects/cycle_analytics/_empty_stage.html.haml7
-rw-r--r--app/views/projects/cycle_analytics/_no_access.html.haml7
-rw-r--r--app/views/projects/cycle_analytics/_overview.html.haml15
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml107
-rw-r--r--app/views/projects/deploy_keys/_deploy_key.html.haml3
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml9
-rw-r--r--app/views/projects/deployments/_actions.haml2
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_content.html.haml6
-rw-r--r--app/views/projects/diffs/_diffs.html.haml9
-rw-r--r--app/views/projects/diffs/_file.html.haml8
-rw-r--r--app/views/projects/diffs/_file_header.html.haml2
-rw-r--r--app/views/projects/diffs/_image.html.haml34
-rw-r--r--app/views/projects/diffs/_line.html.haml8
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml14
-rw-r--r--app/views/projects/diffs/_stats.html.haml12
-rw-r--r--app/views/projects/edit.html.haml128
-rw-r--r--app/views/projects/empty.html.haml15
-rw-r--r--app/views/projects/environments/_environment.html.haml35
-rw-r--r--app/views/projects/environments/_header_title.html.haml1
-rw-r--r--app/views/projects/environments/_terminal_button.html.haml3
-rw-r--r--app/views/projects/environments/index.html.haml57
-rw-r--r--app/views/projects/environments/show.html.haml1
-rw-r--r--app/views/projects/environments/terminal.html.haml22
-rw-r--r--app/views/projects/find_file/show.html.haml6
-rw-r--r--app/views/projects/forks/error.html.haml6
-rw-r--r--app/views/projects/forks/index.html.haml6
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml91
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml10
-rw-r--r--app/views/projects/graphs/ci/_build_times.haml2
-rw-r--r--app/views/projects/graphs/ci/_builds.haml6
-rw-r--r--app/views/projects/graphs/show.html.haml2
-rw-r--r--app/views/projects/group_links/_index.html.haml (renamed from app/views/projects/group_links/index.html.haml)4
-rw-r--r--app/views/projects/group_links/update.js.haml1
-rw-r--r--app/views/projects/hooks/_index.html.haml (renamed from app/views/projects/hooks/index.html.haml)0
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/issues/_issues.html.haml3
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml22
-rw-r--r--app/views/projects/issues/_new_branch.html.haml5
-rw-r--r--app/views/projects/issues/index.html.haml30
-rw-r--r--app/views/projects/issues/show.html.haml3
-rw-r--r--app/views/projects/mattermosts/_no_teams.html.haml12
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml47
-rw-r--r--app/views/projects/mattermosts/new.html.haml8
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml11
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml13
-rw-r--r--app/views/projects/merge_requests/_show.html.haml103
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml24
-rw-r--r--app/views/projects/merge_requests/conflicts/_commit_stats.html.haml8
-rw-r--r--app/views/projects/merge_requests/conflicts/_file_actions.html.haml4
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml2
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml2
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml19
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml10
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_builds.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml14
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml10
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml10
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml10
-rw-r--r--app/views/projects/merge_requests/widget/_closed.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml14
-rw-r--r--app/views/projects/merge_requests/widget/_merged_buttons.haml6
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml23
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml27
-rw-r--r--app/views/projects/merge_requests/widget/open/_archived.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_check.html.haml9
-rw-r--r--app/views/projects/merge_requests/widget/open/_conflicts.html.haml30
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml7
-rw-r--r--app/views/projects/merge_requests/widget/open/_nothing.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml6
-rw-r--r--app/views/projects/milestones/_form.html.haml9
-rw-r--r--app/views/projects/milestones/show.html.haml19
-rw-r--r--app/views/projects/new.html.haml10
-rw-r--r--app/views/projects/notes/_edit_form.html.haml13
-rw-r--r--app/views/projects/notes/_form.html.haml3
-rw-r--r--app/views/projects/notes/_note.html.haml23
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml5
-rw-r--r--app/views/projects/pipelines/_graph.html.haml4
-rw-r--r--app/views/projects/pipelines/_info.html.haml72
-rw-r--r--app/views/projects/pipelines/_stage.html.haml3
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml42
-rw-r--r--app/views/projects/pipelines/index.html.haml49
-rw-r--r--app/views/projects/pipelines/show.html.haml10
-rw-r--r--app/views/projects/pipelines_settings/_badge.html.haml2
-rw-r--r--app/views/projects/pipelines_settings/show.html.haml3
-rw-r--r--app/views/projects/project_members/_index.html.haml22
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml38
-rw-r--r--app/views/projects/project_members/_team.html.haml8
-rw-r--r--app/views/projects/project_members/import.html.haml3
-rw-r--r--app/views/projects/project_members/index.html.haml28
-rw-r--r--app/views/projects/project_members/update.js.haml1
-rw-r--r--app/views/projects/refs/logs_tree.js.haml4
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/projects/services/_form.html.haml20
-rw-r--r--app/views/projects/services/_index.html.haml (renamed from app/views/projects/services/index.html.haml)7
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml91
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml15
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml7
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml95
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml (renamed from app/views/projects/hooks/_project_hook.html.haml)0
-rw-r--r--app/views/projects/settings/integrations/show.html.haml3
-rw-r--r--app/views/projects/settings/members/show.html.haml6
-rw-r--r--app/views/projects/show.html.haml24
-rw-r--r--app/views/projects/snippets/_actions.html.haml14
-rw-r--r--app/views/projects/snippets/index.html.haml20
-rw-r--r--app/views/projects/snippets/show.html.haml7
-rw-r--r--app/views/projects/stage/_graph.html.haml19
-rw-r--r--app/views/projects/stage/_in_stage_group.html.haml14
-rw-r--r--app/views/projects/stage/_stage.html.haml13
-rw-r--r--app/views/projects/tags/_tag.html.haml41
-rw-r--r--app/views/projects/tags/index.html.haml23
-rw-r--r--app/views/projects/tags/new.html.haml4
-rw-r--r--app/views/projects/tags/show.html.haml35
-rw-r--r--app/views/projects/tree/_blob_item.html.haml2
-rw-r--r--app/views/projects/tree/_submodule_item.html.haml2
-rw-r--r--app/views/projects/tree/_tree_content.html.haml11
-rw-r--r--app/views/projects/tree/_tree_header.html.haml8
-rw-r--r--app/views/projects/tree/_tree_item.html.haml2
-rw-r--r--app/views/projects/triggers/index.html.haml20
-rw-r--r--app/views/projects/variables/_table.html.haml4
-rw-r--r--app/views/projects/variables/index.html.haml1
-rw-r--r--app/views/projects/wikis/_form.html.haml3
-rw-r--r--app/views/projects/wikis/_nav.html.haml16
-rw-r--r--app/views/projects/wikis/_new.html.haml4
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml23
-rw-r--r--app/views/projects/wikis/edit.html.haml32
-rw-r--r--app/views/projects/wikis/git_access.html.haml52
-rw-r--r--app/views/projects/wikis/history.html.haml17
-rw-r--r--app/views/projects/wikis/pages.html.haml13
-rw-r--r--app/views/projects/wikis/show.html.haml17
-rw-r--r--app/views/repository_check_mailer/notify.html.haml2
-rw-r--r--app/views/repository_check_mailer/notify.text.haml2
-rw-r--r--app/views/search/_category.html.haml26
-rw-r--r--app/views/search/_filter.html.haml4
-rw-r--r--app/views/search/results/_blob.html.haml12
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/shared/_choose_group_avatar_button.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml6
-rw-r--r--app/views/shared/_commit_message_container.html.haml9
-rw-r--r--app/views/shared/_confirm_modal.html.haml4
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_file_highlight.html.haml4
-rw-r--r--app/views/shared/_group_form.html.haml5
-rw-r--r--app/views/shared/_issues.html.haml4
-rw-r--r--app/views/shared/_label.html.haml57
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/_milestone_expired.html.haml6
-rw-r--r--app/views/shared/_milestones_filter.html.haml6
-rw-r--r--app/views/shared/_nav_scroll.html.haml2
-rw-r--r--app/views/shared/_no_password.html.haml4
-rw-r--r--app/views/shared/_no_ssh.html.haml4
-rw-r--r--app/views/shared/_service_settings.html.haml84
-rw-r--r--app/views/shared/_sort_dropdown.html.haml4
-rw-r--r--app/views/shared/builds/_tabs.html.haml8
-rw-r--r--app/views/shared/empty_states/_issues.html.haml22
-rw-r--r--app/views/shared/empty_states/icons/_issues.svg1
-rw-r--r--app/views/shared/groups/_group.html.haml4
-rw-r--r--app/views/shared/icons/_delta.svg3
-rw-r--r--app/views/shared/icons/_go_logo.svg.erb1
-rw-r--r--app/views/shared/icons/_icon_cycle_analytics_overview.svg81
-rw-r--r--app/views/shared/icons/_icon_lock.svg25
-rw-r--r--app/views/shared/icons/_icon_no_data.svg27
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_canceled.svg7
-rw-r--r--app/views/shared/icons/_icon_status_canceled_borderless.svg1
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_created.svg2
-rw-r--r--app/views/shared/icons/_icon_status_created_borderless.svg1
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_failed.svg7
-rw-r--r--app/views/shared/icons/_icon_status_failed_borderless.svg1
-rwxr-xr-xapp/views/shared/icons/_icon_status_manual.svg1
-rw-r--r--app/views/shared/icons/_icon_status_manual_borderless.svg1
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_pending.svg7
-rw-r--r--app/views/shared/icons/_icon_status_pending_borderless.svg1
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_running.svg7
-rw-r--r--app/views/shared/icons/_icon_status_running_borderless.svg1
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_skipped.svg2
-rw-r--r--app/views/shared/icons/_icon_status_skipped_borderless.svg1
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_success.svg7
-rw-r--r--app/views/shared/icons/_icon_status_success_borderless.svg1
-rwxr-xr-x[-rw-r--r--]app/views/shared/icons/_icon_status_warning.svg7
-rw-r--r--app/views/shared/icons/_icon_status_warning_borderless.svg1
-rw-r--r--app/views/shared/icons/_icon_stopwatch.svg1
-rw-r--r--app/views/shared/icons/_icon_terminal.svg1
-rw-r--r--app/views/shared/icons/_mattermost_logo.svg.erb1
-rw-r--r--app/views/shared/icons/_scroll_down.svg3
-rw-r--r--app/views/shared/icons/_scroll_down_hover_active.svg3
-rw-r--r--app/views/shared/icons/_scroll_up.svg3
-rw-r--r--app/views/shared/icons/_scroll_up_hover_active.svg3
-rw-r--r--app/views/shared/issuable/_filter.html.haml18
-rw-r--r--app/views/shared/issuable/_form.html.haml133
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml4
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml2
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml10
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_nav.html.haml20
-rw-r--r--app/views/shared/issuable/_participants.html.haml4
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml137
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml55
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml30
-rw-r--r--app/views/shared/issuable/form/_description.html.haml15
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml38
-rw-r--r--app/views/shared/issuable/form/_title.html.haml36
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml13
-rw-r--r--app/views/shared/members/_group.html.haml24
-rw-r--r--app/views/shared/members/_member.html.haml26
-rw-r--r--app/views/shared/members/_sort_dropdown.html.haml9
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml15
-rw-r--r--app/views/shared/milestones/_issuable.html.haml6
-rw-r--r--app/views/shared/milestones/_issuables.html.haml8
-rw-r--r--app/views/shared/milestones/_milestone.html.haml4
-rw-r--r--app/views/shared/milestones/_summary.html.haml62
-rw-r--r--app/views/shared/milestones/_tabs.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml4
-rw-r--r--app/views/shared/notifications/_button.html.haml5
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--app/views/shared/notifications/_notification_dropdown.html.haml3
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/snippets/_blob.html.haml2
-rw-r--r--app/views/shared/snippets/_header.html.haml12
-rw-r--r--app/views/shared/snippets/_snippet.html.haml31
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml9
-rw-r--r--app/views/shared/tokens/_scopes_list.html.haml13
-rw-r--r--app/views/shared/web_hooks/_form.html.haml3
-rw-r--r--app/views/sherlock/file_samples/show.html.haml2
-rw-r--r--app/views/sherlock/queries/_general.html.haml4
-rw-r--r--app/views/sherlock/queries/show.html.haml4
-rw-r--r--app/views/sherlock/transactions/index.html.haml2
-rw-r--r--app/views/sherlock/transactions/show.html.haml6
-rw-r--r--app/views/snippets/_actions.html.haml14
-rw-r--r--app/views/snippets/_snippets.html.haml3
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml31
-rw-r--r--app/views/snippets/show.html.haml4
-rw-r--r--app/views/u2f/_authenticate.html.haml19
-rw-r--r--app/views/u2f/_register.html.haml4
-rw-r--r--app/views/users/calendar.html.haml2
-rw-r--r--app/views/users/show.html.haml8
-rw-r--r--app/workers/authorized_projects_worker.rb14
-rw-r--r--app/workers/build_queue_worker.rb10
-rw-r--r--app/workers/build_success_worker.rb4
-rw-r--r--app/workers/delete_merged_branches_worker.rb20
-rw-r--r--app/workers/new_note_worker.rb14
-rw-r--r--app/workers/pipeline_metrics_worker.rb4
-rw-r--r--app/workers/pipeline_success_worker.rb2
-rw-r--r--app/workers/process_commit_worker.rb25
-rw-r--r--app/workers/project_cache_worker.rb57
-rw-r--r--app/workers/reactive_caching_worker.rb15
-rw-r--r--app/workers/use_key_worker.rb13
1263 files changed, 25758 insertions, 11512 deletions
diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt
deleted file mode 100644
index df187637e18..00000000000
--- a/app/assets/fonts/OFL.txt
+++ /dev/null
@@ -1,93 +0,0 @@
-Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
-
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-
-This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-
-
------------------------------------------------------------
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
------------------------------------------------------------
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide
-development of collaborative font projects, to support the font creation
-efforts of academic and linguistic communities, and to provide a free and
-open framework in which fonts may be shared and improved in partnership
-with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and
-redistributed freely as long as they are not sold by themselves. The
-fonts, including any derivative works, can be bundled, embedded,
-redistributed and/or sold with any software provided that any reserved
-names are not used by derivative works. The fonts and derivatives,
-however, cannot be released under any other type of license. The
-requirement for fonts to remain under this license does not apply
-to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-"Font Software" refers to the set of files released by the Copyright
-Holder(s) under this license and clearly marked as such. This may
-include source files, build scripts and documentation.
-
-"Reserved Font Name" refers to any names specified as such after the
-copyright statement(s).
-
-"Original Version" refers to the collection of Font Software components as
-distributed by the Copyright Holder(s).
-
-"Modified Version" refers to any derivative made by adding to, deleting,
-or substituting -- in part or in whole -- any of the components of the
-Original Version, by changing formats or by porting the Font Software to a
-new environment.
-
-"Author" refers to any designer, engineer, programmer, technical
-writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Font Software, to use, study, copy, merge, embed, modify,
-redistribute, and sell modified and unmodified copies of the Font
-Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components,
-in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled,
-redistributed and/or sold with any software, provided that each copy
-contains the above copyright notice and this license. These can be
-included either as stand-alone text files, human-readable headers or
-in the appropriate machine-readable metadata fields within text or
-binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font
-Name(s) unless explicit written permission is granted by the corresponding
-Copyright Holder. This restriction only applies to the primary font name as
-presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
-Software shall not be used to promote, endorse or advertise any
-Modified Version, except to acknowledge the contribution(s) of the
-Copyright Holder(s) and the Author(s) or with their explicit written
-permission.
-
-5) The Font Software, modified or unmodified, in part or in whole,
-must be distributed entirely under this license, and must not be
-distributed under any other license. The requirement for fonts to
-remain under this license does not apply to any document created
-using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are
-not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff b/app/assets/fonts/SourceSansPro-Black.ttf.woff
deleted file mode 100644
index b7e86200927..00000000000
--- a/app/assets/fonts/SourceSansPro-Black.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff2 b/app/assets/fonts/SourceSansPro-Black.ttf.woff2
deleted file mode 100644
index c90d078406c..00000000000
--- a/app/assets/fonts/SourceSansPro-Black.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff
deleted file mode 100644
index c3314b1ef06..00000000000
--- a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2
deleted file mode 100644
index b87e22c41b5..00000000000
--- a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff b/app/assets/fonts/SourceSansPro-Bold.ttf.woff
deleted file mode 100644
index d1d40f840f8..00000000000
--- a/app/assets/fonts/SourceSansPro-Bold.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Bold.ttf.woff2
deleted file mode 100644
index 0f46f3e833a..00000000000
--- a/app/assets/fonts/SourceSansPro-Bold.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff
deleted file mode 100644
index ef6ff514d3a..00000000000
--- a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2
deleted file mode 100644
index 8007df6df32..00000000000
--- a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff
deleted file mode 100644
index 1e6c94d9eb3..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2
deleted file mode 100644
index b715f274082..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff
deleted file mode 100644
index 7a408b1ec73..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2
deleted file mode 100644
index d8f9d29d4aa..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff b/app/assets/fonts/SourceSansPro-It.ttf.woff
deleted file mode 100644
index 4d54bc95718..00000000000
--- a/app/assets/fonts/SourceSansPro-It.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff2 b/app/assets/fonts/SourceSansPro-It.ttf.woff2
deleted file mode 100644
index a00852641f8..00000000000
--- a/app/assets/fonts/SourceSansPro-It.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff b/app/assets/fonts/SourceSansPro-Light.ttf.woff
deleted file mode 100644
index 1706d57d3c5..00000000000
--- a/app/assets/fonts/SourceSansPro-Light.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff2 b/app/assets/fonts/SourceSansPro-Light.ttf.woff2
deleted file mode 100644
index d8b610ad76e..00000000000
--- a/app/assets/fonts/SourceSansPro-Light.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff
deleted file mode 100644
index 87378d6c609..00000000000
--- a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2
deleted file mode 100644
index e0eebac8273..00000000000
--- a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff b/app/assets/fonts/SourceSansPro-Regular.ttf.woff
deleted file mode 100644
index 460ab12a638..00000000000
--- a/app/assets/fonts/SourceSansPro-Regular.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2 b/app/assets/fonts/SourceSansPro-Regular.ttf.woff2
deleted file mode 100644
index 0dd3464c74b..00000000000
--- a/app/assets/fonts/SourceSansPro-Regular.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff
deleted file mode 100644
index 43379631b2d..00000000000
--- a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2 b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2
deleted file mode 100644
index 2526d2e1b60..00000000000
--- a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff
deleted file mode 100644
index 232c2048ae7..00000000000
--- a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2 b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2
deleted file mode 100644
index 606935af089..00000000000
--- a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff2
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/auth_buttons/authentiq_64.png b/app/assets/images/auth_buttons/authentiq_64.png
new file mode 100644
index 00000000000..81767bbcc54
--- /dev/null
+++ b/app/assets/images/auth_buttons/authentiq_64.png
Binary files differ
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
index 82e526ae0ef..8a260aae1b1 100644
--- a/app/assets/javascripts/abuse_reports.js.es6
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign */
+
((global) => {
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
deleted file mode 100644
index 919107b8cb9..00000000000
--- a/app/assets/javascripts/activities.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable */
-(function() {
- this.Activities = (function() {
- function Activities() {
- Pager.init(20, true, false, this.updateTooltips);
- $(".event-filter-link").on("click", (function(_this) {
- return function(event) {
- event.preventDefault();
- _this.toggleFilter($(event.currentTarget));
- return _this.reloadActivities();
- };
- })(this));
- }
-
- Activities.prototype.updateTooltips = function() {
- gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
- };
-
- Activities.prototype.reloadActivities = function() {
- $(".content_list").html('');
- Pager.init(20, true, false, this.updateTooltips);
- };
-
- Activities.prototype.toggleFilter = function(sender) {
- var filter = sender.attr("id").split("_")[0];
-
- $('.event-filter .active').removeClass("active");
- Cookies.set("event_filter", filter);
-
- sender.closest('li').toggleClass("active");
- };
-
- return Activities;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/activities.js.es6 b/app/assets/javascripts/activities.js.es6
new file mode 100644
index 00000000000..648cb4d5d85
--- /dev/null
+++ b/app/assets/javascripts/activities.js.es6
@@ -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/admin.js b/app/assets/javascripts/admin.js
index 1ef340e4ca1..993f427c9fb 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
+/* global Turbolinks */
+
(function() {
this.Admin = (function() {
function Admin() {
@@ -59,7 +61,5 @@
}
return Admin;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1cab66e109e..b4a8c827d7f 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */
+
(function() {
- this.Api = {
+ var Api = {
groupsPath: "/api/:version/groups.json",
groupPath: "/api/:version/groups/:id.json",
namespacesPath: "/api/:version/namespaces.json",
@@ -10,6 +11,7 @@
licensePath: "/api/:version/templates/licenses/:key",
gitignorePath: "/api/:version/templates/gitignores/:key",
gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
+ dockerfilePath: "/api/:version/dockerfiles/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
group: function(group_id, callback) {
var url = Api.buildUrl(Api.groupPath)
@@ -27,9 +29,9 @@
return $.ajax({
url: url,
data: $.extend({
- search: query,
- per_page: 20
- }, options),
+ search: query,
+ per_page: 20
+ }, options),
dataType: "json"
}).done(function(groups) {
return callback(groups);
@@ -71,7 +73,7 @@
return $.ajax({
url: url,
type: "POST",
- data: {'label': data},
+ data: { 'label': data },
dataType: "json"
}).done(function(label) {
return callback(label);
@@ -119,6 +121,10 @@
return callback(file);
});
},
+ dockerfileYml: function(key, callback) {
+ var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
+ $.get(url, callback);
+ },
issueTemplate: function(namespacePath, projectPath, key, type, callback) {
var url = Api.buildUrl(Api.issuableTemplatePath)
.replace(':key', key)
@@ -140,4 +146,5 @@
}
};
+ window.Api = Api;
}).call(this);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 33c1708e1a9..f0615481ed2 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,4 +1,11 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */
+/* global bp */
+/* global Cookies */
+/* global Flash */
+/* global ConfirmDangerModal */
+/* global AwardsHandler */
+/* global Aside */
+
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
@@ -51,20 +58,24 @@
/*= require_directory ./extensions */
/*= require_directory ./lib/utils */
/*= require_directory ./u2f */
+/*= require_directory ./droplab */
/*= require_directory . */
/*= require fuzzaldrin-plus */
+/*= require es6-promise.auto */
(function () {
- document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
- window.addEventListener('hashchange', gl.utils.shiftWindow);
+ document.addEventListener('page:fetch', function () {
+ // Unbind scroll events
+ $(document).off('scroll');
+ // Close any open tooltips
+ $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+ });
- window.onload = function () {
- // Scroll the window to avoid the topnav bar
- // https://github.com/twitter/bootstrap/issues/1768
- if (location.hash) {
- return setTimeout(gl.utils.shiftWindow, 100);
- }
- };
+ window.addEventListener('hashchange', gl.utils.handleLocationHash);
+ window.addEventListener('load', function onLoad() {
+ window.removeEventListener('load', onLoad, false);
+ gl.utils.handleLocationHash();
+ }, false);
$(function () {
var $body = $('body');
@@ -79,7 +90,23 @@
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
- gl.utils.preventDisabledButtons();
+ // `hashchange` is not triggered when link target is already in window.location
+ $body.on('click', 'a[href^="#"]', function() {
+ var href = this.getAttribute('href');
+ if (href.substr(1) === gl.utils.getLocationHash()) {
+ setTimeout(gl.utils.handleLocationHash, 1);
+ }
+ });
+
+ // prevent default action for disabled buttons
+ $('.btn').click(function(e) {
+ if ($(this).hasClass('disabled')) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return false;
+ }
+ });
+
$('.nav-sidebar').niceScroll({
cursoropacitymax: '0.4',
cursorcolor: '#FFF',
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index c7eff27f971..8438de6cdf1 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */
(function() {
this.Aside = (function() {
function Aside() {
@@ -21,7 +21,5 @@
}
return Aside;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index ab09e4475e6..b16a2c0f73a 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
(function() {
this.Autosave = (function() {
function Autosave(field, key) {
@@ -58,7 +58,5 @@
};
return Autosave;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index d7cda977845..629dc267337 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,7 +1,9 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, brace-style, no-underscore-dangle, no-return-assign, camelcase */
+/* global Cookies */
+
(function() {
this.AwardsHandler = (function() {
- var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
+ var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
function AwardsHandler() {
this.aliases = gl.emojiAliases();
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
@@ -132,7 +134,7 @@
return this.decrementCounter($emojiButton, emoji);
} else {
counter = $emojiButton.find('.js-counter');
- counter.text(parseInt(counter.text()) + 1);
+ counter.text(parseInt(counter.text(), 10) + 1);
$emojiButton.addClass('active');
this.addYouToUserList(votesBlock, emoji);
return this.animateEmoji($emojiButton);
@@ -209,10 +211,10 @@
};
AwardsHandler.prototype.toSentence = function(list) {
- if(list.length <= 2){
+ if (list.length <= 2) {
return list.join(' and ');
}
- else{
+ else {
return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
}
};
@@ -337,7 +339,7 @@
if (Cookies.get('frequently_used_emojis')) {
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>");
- for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) {
+ for (i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) {
emoji = frequentlyUsedEmojis[i];
$(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul);
}
@@ -372,7 +374,5 @@
};
return AwardsHandler;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index 074378b9e52..a6bc262b657 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */
+/* global autosize */
/*= require jquery.ba-resize */
/*= require autosize */
@@ -25,5 +26,4 @@
autosize.update($fields);
return $fields.css('resize', 'vertical');
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index a64cefb62bd..6af8f593872 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, max-len */
(function() {
$(function() {
$("body").on("click", ".js-details-target", function() {
@@ -23,5 +23,4 @@
return e.preventDefault();
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 7ff88ecdcaf..d4895011be7 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, max-len */
+
// Quick Submit behavior
//
// When a child field of a form with a `js-quick-submit` class receives a
@@ -73,5 +74,4 @@
return $this.tooltip('hide');
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index 4ac343f876c..ccbd6b993cb 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, no-else-return, consistent-return, max-len */
// Requires Input behavior
//
// When called on a form with input fields with the `required` attribute, the
@@ -59,5 +59,4 @@
return hideOrShowHelpBlock($form);
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 05b213fe3fb..6a49715590c 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */
(function(w) {
$(function() {
// Toggle button. Show/hide content inside parent container.
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
index 37531aaec9b..d3455fa3d8c 100644
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -1,7 +1,8 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, comma-dangle */
+/* global Api */
+
/*= require blob/template_selector */
((global) => {
-
class BlobCiYamlSelector extends gl.TemplateSelector {
requestFile(query) {
return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
@@ -37,5 +38,4 @@
}
global.BlobCiYamlSelectors = BlobCiYamlSelectors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
new file mode 100644
index 00000000000..bdf95017613
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
@@ -0,0 +1,18 @@
+/* global Api */
+/*= require blob/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.es6 b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6
new file mode 100644
index 00000000000..9cee79fa5d5
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6
@@ -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_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 33fb4f8185c..04bfe363929 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, max-len */
+/* global Dropzone */
+
(function() {
this.BlobFileDropzone = (function() {
function BlobFileDropzone(form, method) {
@@ -60,7 +62,5 @@
}
return BlobFileDropzone;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 344fe5dcd94..5fd0857db29 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params */
+/* global Api */
/*= require blob/template_selector */
@@ -18,7 +19,5 @@
};
return BlobGitignoreSelector;
-
})(gl.TemplateSelector);
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index 9e992f7913c..8236457f0f1 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, max-len */
+/* global BlobGitignoreSelector */
+
(function() {
this.BlobGitignoreSelectors = (function() {
function BlobGitignoreSelectors(opts) {
@@ -20,7 +22,5 @@
}
return BlobGitignoreSelectors;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 41a83a56146..7a14eb160d0 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle */
+/* global Api */
/*= require blob/template_selector */
@@ -23,7 +24,5 @@
};
return BlobLicenseSelector;
-
})(gl.TemplateSelector);
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6
index adeb8ba1318..c5067b0feae 100644
--- a/app/assets/javascripts/blob/blob_license_selectors.js.es6
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, no-param-reassign */
+/* global BlobLicenseSelector */
+
((global) => {
class BlobLicenseSelectors {
constructor({ $dropdowns, editor }) {
@@ -18,5 +20,4 @@
}
global.BlobLicenseSelectors = BlobLicenseSelectors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
index 5434a19bcec..7e03ec3b391 100644
--- a/app/assets/javascripts/blob/template_selector.js.es6
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -1,98 +1,101 @@
-/* eslint-disable */
+/* 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();
+ 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);
- }
+ 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;
- }
- });
- }
+ 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());
- }
+ bindEvents() {
+ return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
+ }
- toggleLabel(item) {
- return item.name;
- }
+ 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');
+ onFilenameUpdate() {
+ var filenameMatches;
+ if (!this.$input.length) {
+ return;
}
-
- onClick(item, el, e) {
- e.preventDefault();
- return this.requestFile(item);
+ filenameMatches = this.pattern.test(this.$input.val().trim());
+ if (!filenameMatches) {
+ this.wrapper.addClass('hidden');
+ return;
}
+ return this.wrapper.removeClass('hidden');
+ }
- requestFile(item) {
- // This `requestFile` method is an abstract method that should
- // be added by all subclasses.
- }
+ onClick(item, el, e) {
+ e.preventDefault();
+ return this.requestFile(item);
+ }
- // To be implemented on the extending class
- // e.g.
- // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
- requestFileSuccess(file, { skipFocus } = {}) {
- const oldValue = this.editor.getValue();
- let newValue = file.content;
+ requestFile(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ }
- this.editor.setValue(newValue, 1);
- if (!skipFocus) this.editor.focus();
+ // To be implemented on the extending class
+ // e.g.
+ // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
+ requestFileSuccess(file, { skipFocus } = {}) {
+ if (!file) return;
- if (this.editor instanceof jQuery) {
- this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
- }
- }
+ const oldValue = this.editor.getValue();
+ const newValue = file.content;
- startLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-spinner fa-spin')
- .removeClass('fa-chevron-down');
- }
+ this.editor.setValue(newValue, 1);
+ if (!skipFocus) this.editor.focus();
- stopLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-chevron-down')
- .removeClass('fa-spinner fa-spin');
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
}
}
- global.TemplateSelector = TemplateSelector;
- })(window.gl || ( window.gl = {}));
+ 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_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index b801c10f168..dfad9b2122b 100644
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
+/* global EditBlob */
+/* global NewCommitForm */
+
/*= require_tree . */
(function() {
@@ -9,5 +12,4 @@
var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
new NewCommitForm($('.js-edit-blob-form'));
});
-
}).call(this);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 60840560dd3..079445e8278 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,6 +1,9 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, max-len */
+/* global ace */
+/* global BlobGitignoreSelectors */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.EditBlob = (function() {
function EditBlob(assets_path, ace_mode) {
@@ -33,6 +36,9 @@
new gl.BlobCiYamlSelectors({
editor: this.editor
});
+ new gl.BlobDockerfileSelectors({
+ editor: this.editor
+ });
}
EditBlob.prototype.initModePanesAndLinks = function() {
@@ -57,7 +63,7 @@
content: this.editor.getValue()
}, function(response) {
currentPane.empty().append(response);
- return currentPane.syntaxHighlight();
+ return currentPane.renderGFM();
});
} else {
this.$toggleButton.show();
@@ -78,7 +84,5 @@
};
return EditBlob;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index efb22d38513..f9766471780 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
+/* global Vue */
+/* global BoardService */
+
//= require vue
//= require vue-resource
//= require Sortable
@@ -13,8 +16,8 @@
//= require ./vue_resource_interceptor
$(() => {
- const $boardApp = document.getElementById('board-app'),
- Store = gl.issueBoards.BoardsStore;
+ const $boardApp = document.getElementById('board-app');
+ const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
@@ -22,6 +25,8 @@ $(() => {
gl.IssueBoardsApp.$destroy(true);
}
+ Store.create();
+
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
@@ -37,16 +42,15 @@ $(() => {
issueLinkBase: $boardApp.dataset.issueLinkBase,
detailIssue: Store.detail
},
- init: Store.create.bind(Store),
computed: {
detailIssueVisible () {
return Object.keys(this.detailIssue.issue).length;
- }
+ },
},
created () {
gl.boardService = new BoardService(this.endpoint, this.boardId);
},
- ready () {
+ mounted () {
Store.disabled = this.disabled;
gl.boardService.all()
.then((resp) => {
@@ -60,6 +64,8 @@ $(() => {
}
});
+ this.state.lists = _.sortBy(this.state.lists, 'position');
+
Store.addBlankState();
this.loading = false;
});
@@ -67,9 +73,12 @@ $(() => {
});
gl.IssueBoardsSearch = new Vue({
- el: '#js-boards-seach',
+ el: '#js-boards-search',
data: {
filters: Store.state.filters
+ },
+ mounted () {
+ gl.issueBoards.newListDropdownInit();
}
});
});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 0e03d43872b..a32881116d5 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var */
+/* global Vue */
+/* global Sortable */
+
//= require ./board_blank_state
//= require ./board_delete
//= require ./board_list
@@ -10,6 +13,7 @@
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,
@@ -24,7 +28,6 @@
return {
detailIssue: Store.detail,
filters: Store.state.filters,
- showIssueForm: false
};
},
watch: {
@@ -42,14 +45,28 @@
const issue = this.list.findIssue(this.detailIssue.issue.id);
if (issue) {
+ const offsetLeft = this.$el.offsetLeft;
const boardsList = document.querySelectorAll('.boards-list')[0];
- const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth;
- const left = boardsList.scrollLeft - this.$el.offsetLeft;
+ 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.scrollLeft = right;
+ $(boardsList).animate({
+ scrollLeft: right
+ }, this.sortableOptions.animation);
} else if (left > 0) {
- boardsList.scrollLeft = this.$el.offsetLeft;
+ $(boardsList).animate({
+ scrollLeft: offsetLeft
+ }, this.sortableOptions.animation);
}
}
},
@@ -58,11 +75,11 @@
},
methods: {
showNewIssueForm() {
- this.showIssueForm = !this.showIssueForm;
+ this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}
},
- ready () {
- const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ mounted () {
+ this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
@@ -71,24 +88,17 @@
gl.issueBoards.onEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
- const order = this.sortable.toArray(),
- $board = this.$parent.$refs.board[e.oldIndex + 1],
- list = $board.list;
-
- $board.$destroy(true);
+ const order = this.sortable.toArray();
+ const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
this.$nextTick(() => {
- Store.state.lists.splice(e.newIndex, 0, list);
Store.moveList(list, order);
});
}
}
});
- this.sortable = Sortable.create(this.$el.parentNode, options);
+ this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
},
- beforeDestroy () {
- Store.state.lists.$remove(this.list);
- }
});
})();
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
index 885553690d3..d76314c1892 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, comma-dangle */
+/* global Vue */
+/* global ListLabel */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -12,7 +15,7 @@
new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Doing', color: '#5CB85C' })
]
- }
+ };
},
methods: {
addDefaultLists () {
@@ -30,6 +33,8 @@
});
});
+ Store.state.lists = _.sortBy(Store.state.lists, 'position');
+
// Save the labels
gl.boardService.generateDefaultLists()
.then((resp) => {
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 2f6c03e3538..5fc50280811 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
+/* global Vue */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -6,6 +8,7 @@
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardCard = Vue.extend({
+ template: '#js-board-list-card',
props: {
list: Object,
issue: Object,
@@ -53,10 +56,8 @@
mouseDown () {
this.showDetail = true;
},
- mouseMove () {
- if (this.showDetail) {
- this.showDetail = false;
- }
+ mouseMove() {
+ this.showDetail = false;
},
showIssue (e) {
const targetTagName = e.target.tagName.toLowerCase();
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6
index c45e1926c5c..861600424a5 100644
--- a/app/assets/javascripts/boards/components/board_delete.js.es6
+++ b/app/assets/javascripts/boards/components/board_delete.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, no-alert */
+/* global Vue */
+
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 34fc7694241..630fe084175 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, max-len */
+/* global Vue */
+/* global Sortable */
+
//= require ./board_card
//= require ./board_new_issue
@@ -9,6 +12,7 @@
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
@@ -19,27 +23,27 @@
issues: Array,
loading: Boolean,
issueLinkBase: String,
- showIssueForm: Boolean
},
data () {
return {
scrollOffset: 250,
filters: Store.state.filters,
- showCount: false
+ showCount: false,
+ showIssueForm: false
};
},
watch: {
filters: {
handler () {
this.list.loadingMore = false;
- this.$els.list.scrollTop = 0;
+ 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++;
+ this.list.page += 1;
this.list.getIssues(false);
}
@@ -51,15 +55,20 @@
});
}
},
+ computed: {
+ orderedIssues () {
+ return _.sortBy(this.issues, 'priority');
+ },
+ },
methods: {
listHeight () {
- return this.$els.list.getBoundingClientRect().height;
+ return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight () {
- return this.$els.list.scrollHeight;
+ return this.$refs.list.scrollHeight;
},
scrollTop () {
- return this.$els.list.scrollTop + this.listHeight();
+ return this.$refs.list.scrollTop + this.listHeight();
},
loadNextPage () {
const getIssues = this.list.nextPage();
@@ -72,8 +81,9 @@
}
},
},
- ready () {
+ mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ scroll: document.querySelectorAll('.boards-list')[0],
group: 'issues',
sort: false,
disabled: this.disabled,
@@ -81,23 +91,25 @@
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
- Store.moving.issue = card.issue;
+ 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);
+ gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
+
+ this.$nextTick(() => {
+ e.item.remove();
+ });
},
- onRemove: (e) => {
- this.$refs.issue[e.oldIndex].$destroy(true);
- }
});
- this.sortable = Sortable.create(this.$els.list, options);
+ this.sortable = Sortable.create(this.$refs.list, options);
// Scroll event on list to load more
- this.$els.list.onscroll = () => {
+ 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.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
index 7fc0bfd56f3..2386d3a613c 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars */
+/* global Vue */
+/* global ListIssue */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -7,7 +10,6 @@
gl.issueBoards.BoardNewIssue = Vue.extend({
props: {
list: Object,
- showIssueForm: Boolean
},
data() {
return {
@@ -15,11 +17,6 @@
error: false
};
},
- watch: {
- showIssueForm () {
- this.$els.input.focus();
- }
- },
methods: {
submit(e) {
e.preventDefault();
@@ -37,28 +34,30 @@
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
- $(this.$els.submitButton).enable();
+ $(this.$refs.submitButton).enable();
Store.detail.issue = issue;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
- $(this.$els.submitButton).enable();
+ $(this.$refs.submitButton).enable();
// Remove the issue
this.list.removeIssue(issue);
// Show error message
this.error = true;
- this.showIssueForm = true;
});
this.cancel();
},
cancel() {
- this.showIssueForm = false;
this.title = '';
+ this.$parent.showIssueForm = false;
}
- }
+ },
+ mounted() {
+ this.$refs.input.focus();
+ },
});
})();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
index 4928320d015..02459722bbf 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -1,4 +1,10 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, no-new */
+/* global Vue */
+/* global IssuableContext */
+/* global MilestoneSelect */
+/* global LabelsSelect */
+/* global Sidebar */
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -41,13 +47,13 @@
this.detail.issue = {};
}
},
- ready () {
+ mounted () {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new gl.DueDateSelectors();
new LabelsSelect();
new Sidebar();
- new Subscription('.subscription');
+ gl.Subscription.bindAll('.subscription');
}
});
})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 14f618fd5d5..556826a9148 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -1,5 +1,9 @@
-/* eslint-disable */
-$(() => {
+/* 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) => {
@@ -15,54 +19,58 @@ $(() => {
});
});
- $('.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'));
+ 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);
+ $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 = $('<li />');
+ const $a = $('<a />', {
+ class: (active ? `is-active js-board-list-${active.id}` : ''),
+ text: label.title,
+ href: '#'
+ });
+ const $labelColor = $('<span />', {
+ class: 'dropdown-label-box',
+ style: `background-color: ${label.color}`
});
- },
- renderRow (label) {
- const active = Store.findList('title', label.title),
- $li = $('<li />'),
- $a = $('<a />', {
- class: (active ? `is-active js-board-list-${active.id}` : ''),
- text: label.title,
- href: '#'
- }),
- $labelColor = $('<span />', {
- 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();
+ 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,
+ if (!Store.findList('title', label.title)) {
+ Store.new({
title: label.title,
- color: label.color
- }
- });
+ 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/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
index 9eceac4eddd..7e192e90fe6 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* 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/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index db9a5a8e40a..b6c6d17274f 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
+/* global DocumentTouch */
+
((w) => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -17,18 +19,19 @@
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
- let defaultSortOptions = {
+ const defaultSortOptions = {
+ animation: 200,
forceFallback: true,
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.board-delete, .btn',
- delay: gl.issueBoards.touchEnabled ? 100 : 50,
+ 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;
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index 21d735e8231..31531c3ee34 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -1,4 +1,9 @@
-/* eslint-disable */
+/* 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.id = obj.iid;
@@ -32,12 +37,12 @@ class ListIssue {
}
findLabel (findLabel) {
- return this.labels.filter( label => label.title === findLabel.title )[0];
+ return this.labels.filter(label => label.title === findLabel.title)[0];
}
removeLabel (removeLabel) {
if (removeLabel) {
- this.labels = this.labels.filter( label => removeLabel.title !== label.title );
+ this.labels = this.labels.filter(label => removeLabel.title !== label.title);
}
}
@@ -46,7 +51,7 @@ class ListIssue {
}
getLists () {
- return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
+ return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
}
update (url) {
@@ -55,7 +60,7 @@ class ListIssue {
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 )
+ label_ids: this.labels.map((label) => label.id)
}
};
@@ -66,3 +71,5 @@ class ListIssue {
return Vue.http.patch(url, data);
}
}
+
+window.ListIssue = ListIssue;
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
index 0910fe9a854..9af88d167d6 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, space-before-function-paren */
+
class ListLabel {
constructor (obj) {
this.id = obj.id;
@@ -9,3 +10,5 @@ class ListLabel {
this.priority = (obj.priority !== null) ? obj.priority : Infinity;
}
}
+
+window.ListLabel = ListLabel;
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index b331a26fed5..3dd5f273057 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* 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;
@@ -42,7 +45,8 @@ class List {
}
destroy () {
- gl.issueBoards.BoardsStore.state.lists.$remove(this);
+ 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);
@@ -54,7 +58,7 @@ class List {
nextPage () {
if (this.issuesSize > this.issues.length) {
- this.page++;
+ this.page += 1;
return this.getIssues(false);
}
@@ -62,12 +66,12 @@ class List {
getIssues (emptyIssues = true) {
const filters = this.filters;
- let data = { page: this.page };
+ 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 );
+ data.label_name = data.label_name.filter(label => label !== this.label.title);
}
if (emptyIssues) {
@@ -90,7 +94,7 @@ class List {
newIssue (issue) {
this.addIssue(issue);
- this.issuesSize++;
+ this.issuesSize += 1;
return gl.boardService.newIssue(this.id, issue)
.then((resp) => {
@@ -105,16 +109,20 @@ class List {
});
}
- addIssue (issue, listFrom) {
+ addIssue (issue, listFrom, newIndex) {
if (!this.findIssue(issue.id)) {
- this.issues.push(issue);
+ 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++;
+ this.issuesSize += 1;
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
.then(() => {
listFrom.getIssues(false);
@@ -124,7 +132,7 @@ class List {
}
findIssue (id) {
- return this.issues.filter( issue => issue.id === id )[0];
+ return this.issues.filter(issue => issue.id === id)[0];
}
removeIssue (removeIssue) {
@@ -132,7 +140,7 @@ class List {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
- this.issuesSize--;
+ this.issuesSize -= 1;
issue.removeLabel(this.label);
}
@@ -140,3 +148,5 @@ class List {
});
}
}
+
+window.List = List;
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6
index a48969e19c9..c867b06d320 100644
--- a/app/assets/javascripts/boards/models/milestone.js.es6
+++ b/app/assets/javascripts/boards/models/milestone.js.es6
@@ -1,7 +1,10 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars */
+
class ListMilestone {
- constructor (obj) {
+ constructor(obj) {
this.id = obj.id;
this.title = obj.title;
}
}
+
+window.ListMilestone = ListMilestone;
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
index 583a973fc46..8e9de4d4cbb 100644
--- a/app/assets/javascripts/boards/models/user.js.es6
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -1,9 +1,12 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars */
+
class ListUser {
- constructor (user) {
+ 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.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index f59a2ed7937..ea55158306b 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
+/* global Vue */
+
class BoardService {
constructor (root, boardId) {
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
@@ -45,7 +47,7 @@ class BoardService {
}
getIssuesForList (id, filter = {}) {
- let data = { id };
+ const data = { id };
Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
return this.issues.get(data);
@@ -63,4 +65,6 @@ class BoardService {
issue
});
}
-};
+}
+
+window.BoardService = BoardService;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index 175e034afed..cdf1b09c0a4 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* 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 || {};
@@ -30,8 +33,8 @@
return list;
},
new (listObj) {
- const list = this.addList(listObj),
- backlogList = this.findList('type', 'backlog', 'backlog');
+ const list = this.addList(listObj);
+ const backlogList = this.findList('type', 'backlog', 'backlog');
list
.save()
@@ -39,6 +42,8 @@
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
+
+ this.state.lists = _.sortBy(this.state.lists, 'position');
});
this.removeBlankState();
},
@@ -47,7 +52,7 @@
},
shouldAddBlankState () {
// Decide whether to add the blank state
- return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]);
+ return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
@@ -58,6 +63,8 @@
title: 'Welcome to your Issue Board!',
position: 0
});
+
+ this.state.lists = _.sortBy(this.state.lists, 'position');
},
removeBlankState () {
this.removeList('blank');
@@ -75,30 +82,30 @@
if (!list) return;
- this.state.lists = this.state.lists.filter( list => list.id !== id );
+ 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));
+ const list = this.findList('id', parseInt(id, 10));
list.position = i;
});
listFrom.update();
},
- moveIssueToList (listFrom, listTo, issue) {
- const issueTo = listTo.findIssue(issue.id),
- issueLists = issue.getLists(),
- listLabels = issueLists.map( listIssue => listIssue.label );
+ 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);
+ listTo.addIssue(issue, listFrom, newIndex);
}
if (listTo.type === 'done' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => {
list.removeIssue(issue);
- })
+ });
issue.removeLabels(listLabels);
} else {
listFrom.removeIssue(issue);
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
index 039ca491cf5..f05780167bf 100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -1,120 +1,119 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, strict, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, no-unused-expressions, prefer-arrow-callback, max-len */
(function () {
- 'use strict';
-
- function simulateEvent(el, type, options) {
- var event;
- if (!el) return;
- var ownerDocument = el.ownerDocument;
-
- options = options || {};
-
- if (/^mouse/.test(type)) {
- event = ownerDocument.createEvent('MouseEvents');
- event.initMouseEvent(type, true, true, ownerDocument.defaultView,
- options.button, options.screenX, options.screenY, options.clientX, options.clientY,
- options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
- } else {
- event = ownerDocument.createEvent('CustomEvent');
-
- event.initCustomEvent(type, true, true, ownerDocument.defaultView,
- options.button, options.screenX, options.screenY, options.clientX, options.clientY,
- options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
-
- event.dataTransfer = {
- data: {},
-
- setData: function (type, val) {
- this.data[type] = val;
- },
-
- getData: function (type) {
- return this.data[type];
- }
- };
- }
-
- if (el.dispatchEvent) {
- el.dispatchEvent(event);
- } else if (el.fireEvent) {
- el.fireEvent('on' + type, event);
- }
-
- return event;
- }
-
- function getTraget(target) {
- var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- var children = el.children;
-
- return (
- children[target.index] ||
- children[target.index === 'first' ? 0 : -1] ||
- children[target.index === 'last' ? children.length - 1 : -1]
- );
- }
-
- function getRect(el) {
- var rect = el.getBoundingClientRect();
- var width = rect.right - rect.left;
- var height = rect.bottom - rect.top;
-
- return {
- x: rect.left,
- y: rect.top,
- cx: rect.left + width / 2,
- cy: rect.top + height / 2,
- w: width,
- h: height,
- hw: width / 2,
- wh: height / 2
- };
- }
-
- function simulateDrag(options, callback) {
- options.to.el = options.to.el || options.from.el;
-
- var fromEl = getTraget(options.from);
- var toEl = getTraget(options.to);
+ 'use strict';
+
+ function simulateEvent(el, type, options) {
+ var event;
+ if (!el) return;
+ var ownerDocument = el.ownerDocument;
+
+ options = options || {};
+
+ if (/^mouse/.test(type)) {
+ event = ownerDocument.createEvent('MouseEvents');
+ event.initMouseEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+ } else {
+ event = ownerDocument.createEvent('CustomEvent');
+
+ event.initCustomEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+
+ event.dataTransfer = {
+ data: {},
+
+ setData: function (type, val) {
+ this.data[type] = val;
+ },
+
+ getData: function (type) {
+ return this.data[type];
+ }
+ };
+ }
+
+ if (el.dispatchEvent) {
+ el.dispatchEvent(event);
+ } else if (el.fireEvent) {
+ el.fireEvent('on' + type, event);
+ }
+
+ return event;
+ }
+
+ function getTraget(target) {
+ var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+ var children = el.children;
+
+ return (
+ children[target.index] ||
+ children[target.index === 'first' ? 0 : -1] ||
+ children[target.index === 'last' ? children.length - 1 : -1]
+ );
+ }
+
+ function getRect(el) {
+ var rect = el.getBoundingClientRect();
+ var width = rect.right - rect.left;
+ var height = rect.bottom - rect.top;
+
+ return {
+ x: rect.left,
+ y: rect.top,
+ cx: rect.left + width / 2,
+ cy: rect.top + height / 2,
+ w: width,
+ h: height,
+ hw: width / 2,
+ wh: height / 2
+ };
+ }
+
+ function simulateDrag(options, callback) {
+ options.to.el = options.to.el || options.from.el;
+
+ var fromEl = getTraget(options.from);
+ var toEl = getTraget(options.to);
var scrollable = options.scrollable;
- var fromRect = getRect(fromEl);
- var toRect = getRect(toEl);
-
- var startTime = new Date().getTime();
- var duration = options.duration || 1000;
- simulateEvent(fromEl, 'mousedown', {button: 0});
- options.ontap && options.ontap();
- window.SIMULATE_DRAG_ACTIVE = 1;
-
- var dragInterval = setInterval(function loop() {
- var progress = (new Date().getTime() - startTime) / duration;
- var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
- var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
- var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
-
- simulateEvent(overEl, 'mousemove', {
- clientX: x,
- clientY: y
- });
-
- if (progress >= 1) {
- options.ondragend && options.ondragend();
- simulateEvent(toEl, 'mouseup');
- clearInterval(dragInterval);
- window.SIMULATE_DRAG_ACTIVE = 0;
- }
- }, 100);
-
- return {
- target: fromEl,
- fromList: fromEl.parentNode,
- toList: toEl.parentNode
- };
- }
-
-
- // Export
- window.simulateEvent = simulateEvent;
- window.simulateDrag = simulateDrag;
+ var fromRect = getRect(fromEl);
+ var toRect = getRect(toEl);
+
+ var startTime = new Date().getTime();
+ var duration = options.duration || 1000;
+ simulateEvent(fromEl, 'mousedown', { button: 0 });
+ options.ontap && options.ontap();
+ window.SIMULATE_DRAG_ACTIVE = 1;
+
+ var dragInterval = setInterval(function loop() {
+ var progress = (new Date().getTime() - startTime) / duration;
+ var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
+ var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
+ var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
+
+ simulateEvent(overEl, 'mousemove', {
+ clientX: x,
+ clientY: y
+ });
+
+ if (progress >= 1) {
+ options.ondragend && options.ondragend();
+ simulateEvent(toEl, 'mouseup');
+ clearInterval(dragInterval);
+ window.SIMULATE_DRAG_ACTIVE = 0;
+ }
+ }, 100);
+
+ return {
+ target: fromEl,
+ fromList: fromEl.parentNode,
+ toList: toEl.parentNode
+ };
+ }
+
+ // Export
+ window.simulateEvent = simulateEvent;
+ window.simulateDrag = simulateDrag;
})();
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
index 80f137ca12e..54c2b4ad369 100644
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -1,8 +1,10 @@
-/* eslint-disable */
+/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */
+/* global Vue */
+
Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
next(function (response) {
- Vue.activeResources--;
+ Vue.activeResources -= 1;
});
});
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 5d4d23e26c6..eae062a3aa3 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */
+
(function() {
- this.Breakpoints = (function() {
+ var Breakpoints = (function() {
var BreakpointInstance, instance;
function Breakpoints() {}
@@ -51,7 +52,6 @@
};
return BreakpointInstance;
-
})();
Breakpoints.get = function() {
@@ -59,7 +59,6 @@
};
return Breakpoints;
-
})();
$((function(_this) {
@@ -68,4 +67,5 @@
};
})(this));
+ window.Breakpoints = Breakpoints;
}).call(this);
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index 576f4c76c1e..dbdadc73c3f 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */
(function() {
$(function() {
var previewPath;
@@ -31,5 +31,4 @@
}
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 5133e361001..0df84234520 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,6 +1,11 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
+/* global Breakpoints */
+/* global Turbolinks */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+ var AUTO_SCROLL_OFFSET = 75;
+ var DOWN_BUILD_TRACE = '#down-build-trace';
this.Build = (function() {
Build.interval = null;
@@ -12,10 +17,21 @@
this.pageUrl = options.pageUrl;
this.buildUrl = options.buildUrl;
this.buildStatus = options.buildStatus;
- this.state = options.state1;
+ this.state = options.logState;
this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document);
+ this.$body = $('body');
+ this.$buildTrace = $('#build-trace');
+ this.$autoScrollContainer = $('.autoscroll-container');
+ this.$autoScrollStatus = $('#autoscroll-status');
+ this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
+ this.$upBuildTrace = $('#up-build-trace');
+ this.$downBuildTrace = $(DOWN_BUILD_TRACE);
+ this.$scrollTopBtn = $('#scroll-top');
+ this.$scrollBottomBtn = $('#scroll-bottom');
+ this.$buildRefreshAnimation = $('.js-build-refresh');
+
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
@@ -29,6 +45,7 @@
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+ this.$document.on('scroll', this.initScrollMonitor.bind(this));
$(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
@@ -37,18 +54,6 @@
this.initScrollButtonAffix();
}
if (this.buildStatus === "running" || this.buildStatus === "pending") {
- // Bind autoscroll button to follow build output
- $('#autoscroll-button').on('click', function() {
- var state;
- state = $(this).data("state");
- if ("enabled" === state) {
- $(this).data("state", "disabled");
- return $(this).text("Enable autoscroll");
- } else {
- $(this).data("state", "enabled");
- return $(this).text("Disable autoscroll");
- }
- });
Build.interval = setInterval((function(_this) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
@@ -65,7 +70,7 @@
this.$sidebar = $('.js-build-sidebar');
this.sidebarTranslationLimits = {
min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
- }
+ };
this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight();
this.$sidebar.css({
top: this.sidebarTranslationLimits.max
@@ -80,17 +85,21 @@
};
Build.prototype.getInitialBuildTrace = function() {
- var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
+ var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
return $.ajax({
url: this.buildUrl,
dataType: 'json',
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
+ if (window.location.hash === DOWN_BUILD_TRACE) {
+ $("html,body").scrollTop(this.$buildTrace.height());
+ }
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
- return $('.js-build-refresh').remove();
+ this.$buildRefreshAnimation.remove();
+ return this.initScrollMonitor();
}
- }
+ }.bind(this)
});
};
@@ -100,6 +109,8 @@
dataType: "json",
success: (function(_this) {
return function(log) {
+ var pageUrl;
+
if (log.state) {
_this.state = log.state;
}
@@ -111,7 +122,12 @@
}
return _this.checkAutoscroll();
} else if (log.status !== _this.buildStatus) {
- return Turbolinks.visit(_this.pageUrl);
+ pageUrl = _this.pageUrl;
+ if (_this.$autoScrollStatus.data('state') === 'enabled') {
+ pageUrl += DOWN_BUILD_TRACE;
+ }
+
+ return Turbolinks.visit(pageUrl);
}
};
})(this)
@@ -119,22 +135,95 @@
};
Build.prototype.checkAutoscroll = function() {
- if ("enabled" === $("#autoscroll-button").data("state")) {
- return $("html,body").scrollTop($("#build-trace").height());
+ if (this.$autoScrollStatus.data("state") === "enabled") {
+ return $("html,body").scrollTop(this.$buildTrace.height());
+ }
+
+ // Handle a situation where user started new build
+ // but never scrolled a page
+ if (!this.$scrollTopBtn.is(':visible') &&
+ !this.$scrollBottomBtn.is(':visible') &&
+ !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ this.$scrollBottomBtn.show();
}
};
Build.prototype.initScrollButtonAffix = function() {
- var $body, $buildTrace;
- $body = $('body');
- $buildTrace = $('#build-trace');
- return this.$buildScroll.affix({
- offset: {
- bottom: function() {
- return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
- }
+ // Hide everything initially
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+ this.$autoScrollContainer.hide();
+ };
+
+ // Page scroll listener to detect if user has scrolling page
+ // and handle following cases
+ // 1) User is at Top of Build Log;
+ // - Hide Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ // 2) User is at Bottom of Build Log;
+ // - Show Top Arrow button
+ // - Hide Bottom Arrow button
+ // - Enable Autoscroll and show indicator (when build is running)
+ // 3) User is somewhere in middle of Build Log;
+ // - Show Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ Build.prototype.initScrollMonitor = function() {
+ if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is somewhere in middle of Build Log
+
+ this.$scrollTopBtn.show();
+
+ if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
+ this.$scrollBottomBtn.show();
+ } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
+ this.$scrollBottomBtn.show();
+ } else {
+ this.$scrollBottomBtn.hide();
}
- });
+
+ // Hide Autoscroll Status Indicator
+ if (this.$scrollBottomBtn.is(':visible')) {
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ } else {
+ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollStatusText.addClass('animate');
+ }
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is at Top of Build Log
+
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.show();
+
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
+ (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
+ // User is at Bottom of Build Log
+
+ this.$scrollTopBtn.show();
+ this.$scrollBottomBtn.hide();
+
+ // Show and Reposition Autoscroll Status Indicator
+ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollStatusText.addClass('animate');
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // Build Log height is small
+
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+
+ // Hide Autoscroll Status Indicator
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ }
+
+ if (this.buildStatus === "running" || this.buildStatus === "pending") {
+ // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
+ this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
+ }
};
Build.prototype.shouldHideSidebarForViewport = function() {
@@ -172,7 +261,7 @@
$date = $('.js-artifacts-remove');
if ($date.length) {
date = $date.text();
- return $date.text(gl.utils.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
+ return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
}
};
@@ -193,6 +282,7 @@
};
Build.prototype.stepTrace = function(e) {
+ var $currentTarget;
e.preventDefault();
$currentTarget = $(e.currentTarget);
$.scrollTo($currentTarget.attr('href'), {
@@ -201,7 +291,5 @@
};
return Build;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index 49f84581650..083448552b6 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
(function() {
this.BuildArtifacts = (function() {
function BuildArtifacts() {
@@ -22,7 +22,5 @@
};
return BuildArtifacts;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6
index 0ecd20bc11e..99082b412e2 100644
--- a/app/assets/javascripts/build_variables.js.es6
+++ b/app/assets/javascripts/build_variables.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
-$(function(){
- $('.reveal-variables').off('click').on('click',function(){
+/* 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.es6 b/app/assets/javascripts/ci_lint_editor.js.es6
new file mode 100644
index 00000000000..56ffaa765a8
--- /dev/null
+++ b/app/assets/javascripts/ci_lint_editor.js.es6
@@ -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/commit.js b/app/assets/javascripts/commit.js
index fac5b4f17da..c656ae4e241 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife */
+/* global CommitFile */
+
(function() {
this.Commit = (function() {
function Commit() {
@@ -8,7 +10,5 @@
}
return Commit;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index 16d63729d31..184b4561d2e 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -1,14 +1,14 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */
+/* global ImageFile */
+
(function() {
this.CommitFile = (function() {
function CommitFile(file) {
if ($('.image', file).length) {
- new ImageFile(file);
+ new gl.ImageFile(file);
}
}
return CommitFile;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index ffddce1297b..f09a6b1e676 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,6 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
(function() {
- this.ImageFile = (function() {
+ gl.ImageFile = (function() {
var prepareFrames;
// Width where images must fits in, for 2-up this gets divided by 2
@@ -172,7 +172,5 @@
};
return ImageFile;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index c765d233831..cabeae74ae3 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */
+/* global Pager */
+
(function() {
this.CommitsList = (function() {
function CommitsList() {}
@@ -6,14 +8,16 @@
CommitsList.timer = null;
CommitsList.init = function(limit) {
- $("body").on("click", ".day-commits-table li.commit", function(event) {
- if (event.target.nodeName !== "A") {
+ $("body").on("click", ".day-commits-table li.commit", function(e) {
+ if (e.target.nodeName !== "A") {
location.href = $(this).attr("url");
e.stopPropagation();
return false;
}
});
- Pager.init(limit, false);
+ Pager.init(limit, false, false, function() {
+ gl.utils.localTimeAgo($('.js-timeago'));
+ });
this.content = $("#commits-list");
this.searchField = $("#commits-search");
return this.initSearch();
@@ -54,7 +58,5 @@
};
return CommitsList;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index 61cc91c524b..9591df70e9c 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
(function() {
this.Compare = (function() {
function Compare(opts) {
@@ -87,7 +87,5 @@
};
return Compare;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
index bd980f87e72..3587431ab69 100644
--- a/app/assets/javascripts/compare_autocomplete.js.es6
+++ b/app/assets/javascripts/compare_autocomplete.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* 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() {
@@ -27,7 +28,6 @@
selectable: true,
filterable: true,
filterByText: true,
- toggleLabel: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
@@ -54,11 +54,16 @@
$('.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/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 143d21adb37..35d98492012 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
(function() {
this.ConfirmDangerModal = (function() {
function ConfirmDangerModal(form, text) {
@@ -27,7 +27,5 @@
}
return ConfirmDangerModal;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 7808d7fe313..3485f8f91ed 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
+/* global Clipboard */
/*= require clipboard */
@@ -6,7 +7,7 @@
var genericError, genericSuccess, showTooltip;
genericSuccess = function(e) {
- showTooltip(e.trigger, 'Copied!');
+ showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
return $(e.trigger).blur();
@@ -31,7 +32,7 @@
var originalTitle = $target.data('original-title');
$target
- .attr('title', 'Copied!')
+ .attr('title', 'Copied')
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
@@ -45,5 +46,4 @@
clipboard.on('success', genericSuccess);
return clipboard.on('error', genericError);
});
-
}).call(this);
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
index 744aa0afa03..947c129d5b5 100644
--- a/app/assets/javascripts/create_label.js.es6
+++ b/app/assets/javascripts/create_label.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* 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) {
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
new file mode 100644
index 00000000000..b83a4c63fad
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js.es6
@@ -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: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="mergeRequest in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="mergeRequest.author.avatarUrl">
+ <h5 class="item-title merge-merquest-title">
+ <a :href="mergeRequest.url">
+ {{ mergeRequest.title }}
+ </a>
+ </h5>
+ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="mergeRequest.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(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
new file mode 100644
index 00000000000..cb1687dcc7a
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js.es6
@@ -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: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="issue in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="issue.author.avatarUrl">
+ <h5 class="item-title issue-title">
+ <a class="issue-title" :href="issue.url">
+ {{ issue.title }}
+ </a>
+ </h5>
+ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="issue.author.webUrl" class="issue-author-link">
+ {{ issue.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="issue.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(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
new file mode 100644
index 00000000000..513298ba4e7
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
@@ -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: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="commit in items" class="stage-event-item">
+ <div class="item-details item-conmmit-component">
+ <img class="avatar" :src="commit.author.avatarUrl">
+ <h5 class="item-title commit-title">
+ <a :href="commit.commitUrl">
+ {{ commit.title }}
+ </a>
+ </h5>
+ <span>
+ First
+ <span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span>
+ <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
+ pushed by
+ <a :href="commit.author.webUrl" class="commit-author-link">
+ {{ commit.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="commit.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(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
new file mode 100644
index 00000000000..73f4205b578
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js.es6
@@ -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: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="issue in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="issue.author.avatarUrl">
+ <h5 class="item-title issue-title">
+ <a class="issue-title" :href="issue.url">
+ {{ issue.title }}
+ </a>
+ </h5>
+ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="issue.author.webUrl" class="issue-author-link">
+ {{ issue.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="issue.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(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
new file mode 100644
index 00000000000..501ffb1fac9
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js.es6
@@ -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: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="mergeRequest in items" class="stage-event-item">
+ <div class="item-details">
+ <img class="avatar" :src="mergeRequest.author.avatarUrl">
+ <h5 class="item-title merge-merquest-title">
+ <a :href="mergeRequest.url">
+ {{ mergeRequest.title }}
+ </a>
+ </h5>
+ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
+ &middot;
+ <span>
+ Opened
+ <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+ </span>
+ <span>
+ by
+ <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+ </span>
+ <template v-if="mergeRequest.state === 'closed'">
+ <span class="merge-request-state">
+ <i class="fa fa-ban"></i>
+ {{ mergeRequest.state.toUpperCase() }}
+ </span>
+ </template>
+ <template v-else>
+ <span class="merge-request-branch" v-if="mergeRequest.branch">
+ <i class= "fa fa-code-fork"></i>
+ <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
+ </span>
+ </template>
+ </div>
+ <div class="item-time">
+ <total-time :time="mergeRequest.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(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
new file mode 100644
index 00000000000..82622232f64
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js.es6
@@ -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: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="build in items" class="stage-event-item item-build-component">
+ <div class="item-details">
+ <img class="avatar" :src="build.author.avatarUrl">
+ <h5 class="item-title">
+ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+ <i class="fa fa-code-fork"></i>
+ <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+ <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
+ <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+ </h5>
+ <span>
+ <a :href="build.url" class="build-date">{{ build.date }}</a>
+ by
+ <a :href="build.author.webUrl" class="issue-author-link">
+ {{ build.author.name }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="build.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(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
new file mode 100644
index 00000000000..4bfd363a1f1
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js.es6
@@ -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: `
+ <div>
+ <div class="events-description">
+ {{ stage.description }}
+ </div>
+ <ul class="stage-event-list">
+ <li v-for="build in items" class="stage-event-item item-build-component">
+ <div class="item-details">
+ <h5 class="item-title">
+ <span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span>
+ <a :href="build.url" class="item-build-name">{{ build.name }}</a>
+ &middot;
+ <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+ <i class="fa fa-code-fork"></i>
+ <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+ <span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
+ <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+ </h5>
+ <span>
+ <a :href="build.url" class="issue-date">
+ {{ build.date }}
+ </a>
+ </span>
+ </div>
+ <div class="item-time">
+ <total-time :time="build.totalTime"></total-time>
+ </div>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(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
new file mode 100644
index 00000000000..0d85e1a4678
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js.es6
@@ -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: `
+ <span class="total-time">
+ <template v-if="Object.keys(time).length">
+ <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
+ <template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
+ <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
+ <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
+ </template>
+ <template v-else>
+ --
+ </template>
+ </span>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
index 331f0209888..2f810a69758 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
@@ -1,98 +1,125 @@
-/* eslint-disable */
-//= require vue
-
-((global) => {
-
- const COOKIE_NAME = 'cycle_analytics_help_dismissed';
- const store = gl.cycleAnalyticsStore = {
- isLoading: true,
- hasError: false,
- isHelpDismissed: Cookies.get(COOKIE_NAME),
- analytics: {}
- };
+/* global Vue */
+/* global Cookies */
+/* global Flash */
- gl.CycleAnalytics = class CycleAnalytics {
- constructor() {
- const that = this;
-
- this.vue = new Vue({
- el: '#cycle-analytics',
- name: 'CycleAnalytics',
- created: this.fetchData(),
- data: store,
- methods: {
- dismissLanding() {
- that.dismissLanding();
- }
- }
- });
- }
-
- fetchData(options) {
- store.isLoading = true;
- options = options || { startDate: 30 };
-
- $.ajax({
- url: $('#cycle-analytics').data('request-path'),
- method: 'GET',
- dataType: 'json',
- contentType: 'application/json',
- data: {
- cycle_analytics: {
- start_date: options.startDate
- }
+//= require vue
+//= require_tree ./svg
+//= require_tree .
+
+$(() => {
+ 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;
}
- }).done((data) => {
- this.decorateData(data);
- this.initDropdown();
- })
- .error((data) => {
- this.handleError(data);
- })
- .always(() => {
- store.isLoading = false;
- })
- }
-
- decorateData(data) {
- data.summary = data.summary || [];
- data.stats = data.stats || [];
-
- data.summary.forEach((item) => {
- item.value = item.value || '-';
- });
-
- data.stats.forEach((item) => {
- item.value = item.value || '- - -';
- });
-
- store.analytics = data;
- }
-
- handleError(data) {
- store.hasError = true;
- new Flash('There was an error while fetching cycle analytics data.', 'alert');
- }
-
- dismissLanding() {
- store.isHelpDismissed = true;
- Cookies.set(COOKIE_NAME, true);
- }
-
- 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);
- const value = $target.data('value');
-
- $label.text($target.text().trim());
- this.fetchData({ startDate: value });
- })
- }
-
- }
-})(window.gl || (window.gl = {}));
+ 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.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6
new file mode 100644
index 00000000000..9f74b14c4b9
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js.es6
@@ -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_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
new file mode 100644
index 00000000000..be732971c7f
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6
@@ -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/svg/icon_branch.js.es6 b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6
new file mode 100644
index 00000000000..5d486bcaf66
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_branch.js.es6
@@ -0,0 +1,7 @@
+/* eslint-disable no-param-reassign */
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+ global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
+
+ global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>';
+})(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
new file mode 100644
index 00000000000..661bf9e9f1c
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_build_status.js.es6
@@ -0,0 +1,7 @@
+/* eslint-disable no-param-reassign */
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+ global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
+
+ global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>';
+})(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
new file mode 100644
index 00000000000..2208c27a619
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/svg/icon_commit.js.es6
@@ -0,0 +1,7 @@
+/* eslint-disable no-param-reassign */
+((global) => {
+ global.cycleAnalytics = global.cycleAnalytics || {};
+ global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
+
+ global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>';
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
deleted file mode 100644
index 4ddafff428f..00000000000
--- a/app/assets/javascripts/diff.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/* eslint-disable */
-(function() {
- this.Diff = (function() {
- var UNFOLD_COUNT;
-
- UNFOLD_COUNT = 20;
-
- function Diff() {
- $('.files .diff-file').singleFileDiff();
- this.filesCommentButton = $('.files .diff-file').filesCommentButton();
- if (this.diffViewType() === 'parallel') {
- $('.content-wrapper .container-fluid').removeClass('container-limited');
- }
- $(document).off('click', '.js-unfold');
- $(document).on('click', '.js-unfold', (function(_this) {
- return function(event) {
- var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom;
- target = $(event.target);
- unfoldBottom = target.hasClass('js-unfold-bottom');
- unfold = true;
- ref = _this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1];
- offset = line_number - old_line;
- if (unfoldBottom) {
- line_number += 1;
- since = line_number;
- to = line_number + UNFOLD_COUNT;
- } else {
- ref1 = _this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1];
- line_number -= 1;
- to = line_number;
- if (line_number - UNFOLD_COUNT > prev_new_line + 1) {
- since = line_number - UNFOLD_COUNT;
- } else {
- since = prev_new_line + 1;
- unfold = false;
- }
- }
- file = target.parents('.diff-file');
- link = file.data('blob-diff-path');
- params = {
- since: since,
- to: to,
- bottom: unfoldBottom,
- offset: offset,
- unfold: unfold,
- // indent is used to compensate for single space indent to fit
- // '+' and '-' prepended to diff lines,
- // see https://gitlab.com/gitlab-org/gitlab-ce/issues/707
- indent: 1,
- view: file.data('view')
- };
- return $.get(link, params, function(response) {
- return target.parent().replaceWith(response);
- });
- };
- })(this));
- }
-
- Diff.prototype.diffViewType = function() {
- return $('.inline-parallel-buttons a.active').data('view-type');
- }
-
- Diff.prototype.lineNumbers = function(line) {
- if (!line.children().length) {
- return [0, 0];
- }
-
- return line.find('.diff-line-num').map(function() {
- return parseInt($(this).data('linenumber'));
- });
- };
-
- return Diff;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6
new file mode 100644
index 00000000000..5e1a4c948aa
--- /dev/null
+++ b/app/assets/javascripts/diff.js.es6
@@ -0,0 +1,121 @@
+/* eslint-disable class-methods-use-this */
+
+(() => {
+ const UNFOLD_COUNT = 20;
+
+ 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');
+ }
+
+ $(document)
+ .off('click', '.js-unfold, .diff-line-num a')
+ .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
+ .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
+
+ 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 $diffFiles = $('.diff-file');
+ $diffFiles.find('.hll').removeClass('hll');
+
+ if (window.location.hash !== '') {
+ const hash = window.location.hash.replace('#', '');
+ $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.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
index 29a12a2395b..2514459e65e 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -1,9 +1,16 @@
-/* eslint-disable */
-((w) => {
- w.CommentAndResolveBtn = Vue.extend({
+/* 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,
- textareaIsEmpty: Boolean
+ },
+ data() {
+ return {
+ textareaIsEmpty: true
+ };
},
computed: {
discussion: function () {
@@ -35,7 +42,7 @@
}
}
},
- ready: function () {
+ mounted: function () {
const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
this.textareaIsEmpty = $textarea.val() === '';
@@ -47,4 +54,6 @@
$(`#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.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
index 983e554b9c1..c3898873eaa 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -1,6 +1,10 @@
-/* eslint-disable */
+/* 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 */
+
(() => {
- JumpToDiscussion = Vue.extend({
+ const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
props: {
discussionId: String
@@ -42,13 +46,13 @@
},
methods: {
jumpToNextUnresolvedDiscussion: function () {
- let discussionsSelector,
- discussionIdsInScope,
- firstUnresolvedDiscussionId,
- nextUnresolvedDiscussionId,
- activeTab = window.mrTabs.currentAction,
- hasDiscussionsToJumpTo = true,
- jumpToFirstDiscussion = !this.discussionId;
+ 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() {
@@ -64,11 +68,11 @@
let unresolvedDiscussionCount = 0;
- for (let i = 0; i < discussionIdsInScope.length; i++) {
+ for (let i = 0; i < discussionIdsInScope.length; i += 1) {
const discussionId = discussionIdsInScope[i];
const discussion = discussions[discussionId];
if (discussion && !discussion.isResolved()) {
- unresolvedDiscussionCount++;
+ unresolvedDiscussionCount += 1;
}
}
@@ -105,7 +109,7 @@
}
let currentDiscussionFound = false;
- for (let i = 0; i < discussionIdsInScope.length; i++) {
+ for (let i = 0; i < discussionIdsInScope.length; i += 1) {
const discussionId = discussionIdsInScope[i];
const discussion = discussions[discussionId];
@@ -152,7 +156,7 @@
// 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')
+ $target.find('.js-toggle-button i').trigger('click');
}
} else if (activeTab === 'diffs') {
// Resolved discussions are hidden in the diffs tab by default.
@@ -166,7 +170,7 @@
// 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++) {
+ 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.
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index bcc052c7c8c..5852b8bbdb7 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,6 +1,11 @@
-/* eslint-disable */
-((w) => {
- w.ResolveBtn = Vue.extend({
+/* 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,
@@ -54,9 +59,11 @@
},
methods: {
updateTooltip: function () {
- $(this.$els.button)
- .tooltip('hide')
- .tooltip('fixTitle');
+ this.$nextTick(() => {
+ $(this.$refs.button)
+ .tooltip('hide')
+ .tooltip('fixTitle');
+ });
},
resolve: function () {
if (!this.canResolve) return;
@@ -85,12 +92,12 @@
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
}
- this.$nextTick(this.updateTooltip);
+ this.updateTooltip();
});
}
},
- compiled: function () {
- $(this.$els.button).tooltip({
+ mounted: function () {
+ $(this.$refs.button).tooltip({
container: 'body'
});
},
@@ -101,4 +108,6 @@
CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
}
});
-})(window);
+
+ Vue.component('resolve-btn', ResolveBtn);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
index 24a99e23132..72cdae812bc 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
+/* global Vue */
+/* global DiscussionMixins */
+/* global CommentsStore */
+
((w) => {
w.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
@@ -13,6 +17,9 @@
computed: {
allResolved: function () {
return this.resolvedDiscussionCount === this.discussionCount;
+ },
+ resolvedCountText() {
+ return this.discussionCount === 1 ? 'discussion' : 'discussions';
}
}
});
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
index 060034f049b..ee5f62b2d9e 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -1,6 +1,10 @@
-/* eslint-disable */
-((w) => {
- w.ResolveDiscussionBtn = Vue.extend({
+/* 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,
@@ -54,4 +58,6 @@
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
}
});
-})(window);
+
+ Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
+})();
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
index 6149bfd052a..1b3a57d0962 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
-//= require vue
-//= require vue-resource
+/* eslint-disable func-names, comma-dangle, new-cap, no-new */
+/* global Vue */
+/* global ResolveCount */
+
//= require_directory ./models
//= require_directory ./stores
//= require_directory ./services
@@ -8,24 +9,35 @@
//= require_directory ./components
$(() => {
- window.DiffNotesApp = new Vue({
- el: '#diff-notes-app',
- components: {
- 'resolve-btn': ResolveBtn,
- 'resolve-discussion-btn': ResolveDiscussionBtn,
- 'comment-and-resolve-btn': CommentAndResolveBtn
- },
- methods: {
- compileComponents: function () {
- const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion');
- if ($components.length) {
- $components.each(function () {
- DiffNotesApp.$compile($(this).get(0));
- });
+ 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',
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
index 7a929017f36..3c08c222f46 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */
+
((w) => {
w.DiscussionMixins = {
computed: {
@@ -12,7 +13,7 @@
const discussion = this.discussions[discussionId];
if (discussion.isResolved()) {
- resolvedCount++;
+ resolvedCount += 1;
}
}
@@ -25,7 +26,7 @@
const discussion = this.discussions[discussionId];
if (!discussion.isResolved()) {
- unresolvedCount++;
+ unresolvedCount += 1;
}
}
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
index 439f55520ef..fa518ba4d33 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* 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;
@@ -57,16 +60,19 @@ class DiscussionModel {
}
updateHeadline (data) {
- const $discussionHeadline = $(`.discussion[data-discussion-id="${this.id}"] .js-discussion-headline`);
+ 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 {
- $(`.discussion[data-discussion-id="${this.id}"] .discussion-header`).append(data.discussion_headline_html);
+ $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html);
}
+
+ gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`));
} else {
- $discussionHeadline.remove();
+ $discussionHeadline.remove();
}
}
@@ -74,7 +80,7 @@ class DiscussionModel {
if (!this.canResolve) {
return false;
}
-
+
for (const noteId in this.notes) {
const note = this.notes[noteId];
@@ -86,3 +92,5 @@ class DiscussionModel {
return false;
}
}
+
+window.DiscussionModel = DiscussionModel;
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
index d0541b02632..f3a7cba5ef6 100644
--- a/app/assets/javascripts/diff_notes/models/note.js.es6
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable camelcase, no-unused-vars */
+
class NoteModel {
- constructor (discussionId, noteId, canResolve, resolved, resolved_by) {
+ constructor(discussionId, noteId, canResolve, resolved, resolved_by) {
this.discussionId = discussionId;
this.id = noteId;
this.canResolve = canResolve;
@@ -8,3 +9,5 @@ class NoteModel {
this.resolved_by = resolved_by;
}
}
+
+window.NoteModel = NoteModel;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index 86953ce7ffb..a52c476352d 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* 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() {
@@ -28,8 +32,8 @@
}
toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId],
- isResolved = discussion.isResolved();
+ const discussion = CommentsStore.state[discussionId];
+ const isResolved = discussion.isResolved();
let promise;
if (isResolved) {
@@ -55,7 +59,7 @@
} else {
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
}
- })
+ });
}
resolveAll(projectPath, mergeRequestId, discussionId) {
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
index f42ca406bb1..c80d979b977 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js.es6
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* 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: {},
@@ -38,7 +41,7 @@
}
},
unresolvedDiscussionIds: function () {
- let ids = [];
+ const ids = [];
for (const discussionId in this.state) {
const discussion = this.state[discussionId];
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 756a24cc0fc..dcf67a8fd68 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -1,4 +1,42 @@
-/* eslint-disable */
+/* 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 GLForm */
+/* global IssuableForm */
+/* global LabelsSelect */
+/* global MilestoneSelect */
+/* global MergedButtons */
+/* global Commit */
+/* global NotificationsForm */
+/* global TreeView */
+/* global NotificationsDropdown */
+/* global UsersSelect */
+/* global GroupAvatar */
+/* global LineHighlighter */
+/* global ShortcutsBlob */
+/* 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 */
+
(function() {
var Dispatcher;
@@ -24,6 +62,18 @@
switch (page) {
case 'sessions:new':
new UsernameValidator();
+ new ActiveTabMemoizer();
+ break;
+ case 'sessions:create':
+ if (!gon.u2f) break;
+ window.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'),
+ );
+ window.gl.u2fAuthenticate.start();
break;
case 'projects:boards:show':
case 'projects:boards:index':
@@ -34,8 +84,13 @@
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
+ if (gl.FilteredSearchManager) {
+ new gl.FilteredSearchManager();
+ }
Issuable.init();
- new gl.IssuableBulkActions();
+ new gl.IssuableBulkActions({
+ prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
+ });
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:issues:show':
@@ -61,7 +116,7 @@
new ZenMode();
break;
case 'projects:compare:show':
- new Diff();
+ new gl.Diff();
break;
case 'projects:issues:new':
case 'projects:issues:edit':
@@ -74,7 +129,7 @@
break;
case 'projects:merge_requests:new':
case 'projects:merge_requests:edit':
- new Diff();
+ new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
@@ -91,38 +146,35 @@
new GLForm($('.release-form'));
break;
case 'projects:merge_requests:show':
- new Diff();
+ new gl.Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
new MergedButtons();
break;
case 'projects:merge_requests:commits':
- case 'projects:merge_requests:builds':
new MergedButtons();
break;
case "projects:merge_requests:diffs":
- new Diff();
+ new gl.Diff();
new ZenMode();
new MergedButtons();
break;
- case 'projects:merge_requests:index':
- shortcut_handler = new ShortcutsNavigation();
- Issuable.init();
- break;
case 'dashboard:activity':
- new Activities();
+ new gl.Activities();
break;
case 'dashboard:projects:starred':
- new Activities();
+ new gl.Activities();
break;
case 'projects:commit:show':
new Commit();
- new Diff();
+ new gl.Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
break;
- case 'projects:commit:builds':
- new gl.Pipelines();
+ case 'projects:commit:pipelines':
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-table',
+ });
break;
case 'projects:commits:show':
case 'projects:activity':
@@ -135,11 +187,21 @@
new TreeView();
}
break;
+ case 'projects:pipelines:builds':
case 'projects:pipelines:show':
- new gl.Pipelines();
+ 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 Activities();
+ new gl.Activities();
break;
case 'groups:show':
shortcut_handler = new ShortcutsNavigation();
@@ -151,7 +213,9 @@
new gl.Members();
new UsersSelect();
break;
- case 'projects:project_members:index':
+ case 'projects:members:show':
+ new gl.MemberExpirationDate('.js-access-expiration-date-groups');
+ new GroupsSelect();
new gl.MemberExpirationDate();
new gl.Members();
new UsersSelect();
@@ -197,10 +261,6 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
- case 'projects:group_links:index':
- new gl.MemberExpirationDate();
- new GroupsSelect();
- break;
case 'search:show':
new Search();
break;
@@ -208,8 +268,12 @@
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
- case 'projects:cycle_analytics:show':
- new gl.CycleAnalytics();
+ case 'projects:variables:index':
+ new gl.ProjectVariables();
+ break;
+ case 'ci:lints:create':
+ case 'ci:lints:show':
+ new gl.CILintEditor();
break;
}
switch (path.first()) {
@@ -262,7 +326,7 @@
new NotificationsDropdown();
break;
case 'wikis':
- new Wikis();
+ new gl.Wikis();
shortcut_handler = new ShortcutsNavigation();
new ZenMode();
new GLForm($('.wiki-form'));
@@ -308,7 +372,5 @@
};
return Dispatcher;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js
new file mode 100644
index 00000000000..c79f0230951
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab.js
@@ -0,0 +1,710 @@
+/* eslint-disable */
+// Determine where to place this
+if (typeof Object.assign != 'function') {
+ Object.assign = function (target, varArgs) { // .length of function is 2
+ 'use strict';
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ var to = Object(target);
+
+ for (var index = 1; index < arguments.length; index++) {
+ var nextSource = arguments[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (var nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ };
+}
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+var DATA_TRIGGER = 'data-dropdown-trigger';
+var DATA_DROPDOWN = 'data-dropdown';
+
+module.exports = {
+ DATA_TRIGGER: DATA_TRIGGER,
+ DATA_DROPDOWN: DATA_DROPDOWN,
+}
+
+},{}],2:[function(require,module,exports){
+// Custom event support for IE
+if ( typeof CustomEvent === "function" ) {
+ module.exports = CustomEvent;
+} else {
+ require('./window')(function(w){
+ var CustomEvent = function ( event, params ) {
+ params = params || { bubbles: false, cancelable: false, detail: undefined };
+ var evt = document.createEvent( 'CustomEvent' );
+ evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
+ return evt;
+ }
+ CustomEvent.prototype = w.Event.prototype;
+
+ w.CustomEvent = CustomEvent;
+ });
+ module.exports = CustomEvent;
+}
+
+},{"./window":11}],3:[function(require,module,exports){
+var CustomEvent = require('./custom_event_polyfill');
+var utils = require('./utils');
+
+var DropDown = function(list) {
+ this.hidden = true;
+ this.list = list;
+ this.items = [];
+ this.getItems();
+ this.initTemplateString();
+ this.addEvents();
+ this.initialState = list.innerHTML;
+};
+
+Object.assign(DropDown.prototype, {
+ getItems: function() {
+ this.items = [].slice.call(this.list.querySelectorAll('li'));
+ return this.items;
+ },
+
+ initTemplateString: function() {
+ var items = this.items || this.getItems();
+
+ var templateString = '';
+ if(items.length > 0) {
+ templateString = items[items.length - 1].outerHTML;
+ }
+ this.templateString = templateString;
+ return this.templateString;
+ },
+
+ clickEvent: function(e) {
+ // climb up the tree to find the LI
+ var selected = utils.closest(e.target, 'LI');
+
+ if(selected) {
+ e.preventDefault();
+ this.hide();
+ var listEvent = new CustomEvent('click.dl', {
+ detail: {
+ list: this,
+ selected: selected,
+ data: e.target.dataset,
+ },
+ });
+ this.list.dispatchEvent(listEvent);
+ }
+ },
+
+ addEvents: function() {
+ this.clickWrapper = this.clickEvent.bind(this);
+ // event delegation.
+ this.list.addEventListener('click', this.clickWrapper);
+ },
+
+ toggle: function() {
+ if(this.hidden) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ },
+
+ setData: function(data) {
+ this.data = data;
+ this.render(data);
+ },
+
+ addData: function(data) {
+ this.data = (this.data || []).concat(data);
+ this.render(this.data);
+ },
+
+ // call render manually on data;
+ render: function(data){
+ // debugger
+ // empty the list first
+ var templateString = this.templateString;
+ var newChildren = [];
+ var toAppend;
+
+ newChildren = (data ||[]).map(function(dat){
+ var html = utils.t(templateString, dat);
+ var template = document.createElement('div');
+ template.innerHTML = html;
+
+ // Help set the image src template
+ var imageTags = template.querySelectorAll('img[data-src]');
+ // debugger
+ for(var i = 0; i < imageTags.length; i++) {
+ var imageTag = imageTags[i];
+ imageTag.src = imageTag.getAttribute('data-src');
+ imageTag.removeAttribute('data-src');
+ }
+
+ if(dat.hasOwnProperty('droplab_hidden') && dat.droplab_hidden){
+ template.firstChild.style.display = 'none'
+ }else{
+ template.firstChild.style.display = 'block';
+ }
+ return template.firstChild.outerHTML;
+ });
+ toAppend = this.list.querySelector('ul[data-dynamic]');
+ if(toAppend) {
+ toAppend.innerHTML = newChildren.join('');
+ } else {
+ this.list.innerHTML = newChildren.join('');
+ }
+ },
+
+ show: function() {
+ // debugger
+ this.list.style.display = 'block';
+ this.hidden = false;
+ },
+
+ hide: function() {
+ // debugger
+ this.list.style.display = 'none';
+ this.hidden = true;
+ },
+
+ destroy: function() {
+ this.hide();
+ this.list.removeEventListener('click', this.clickWrapper);
+ }
+});
+
+module.exports = DropDown;
+
+},{"./custom_event_polyfill":2,"./utils":10}],4:[function(require,module,exports){
+require('./window')(function(w){
+ module.exports = function(deps) {
+ deps = deps || {};
+ var window = deps.window || w;
+ var document = deps.document || window.document;
+ var CustomEvent = deps.CustomEvent || require('./custom_event_polyfill');
+ var HookButton = deps.HookButton || require('./hook_button');
+ var HookInput = deps.HookInput || require('./hook_input');
+ var utils = deps.utils || require('./utils');
+ var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
+
+ var DropLab = function(hook){
+ if (!(this instanceof DropLab)) return new DropLab(hook);
+ this.ready = false;
+ this.hooks = [];
+ this.queuedData = [];
+ this.config = {};
+ this.loadWrapper;
+ if(typeof hook !== 'undefined'){
+ this.addHook(hook);
+ }
+ };
+
+
+ Object.assign(DropLab.prototype, {
+ load: function() {
+ this.loadWrapper();
+ },
+
+ loadWrapper: function(){
+ var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']'));
+ this.addHooks(dropdownTriggers).init();
+ },
+
+ addData: function () {
+ var args = [].slice.apply(arguments);
+ this.applyArgs(args, '_addData');
+ },
+
+ setData: function() {
+ var args = [].slice.apply(arguments);
+ this.applyArgs(args, '_setData');
+ },
+
+ destroy: function() {
+ for(var i = 0; i < this.hooks.length; i++) {
+ this.hooks[i].destroy();
+ }
+ this.hooks = [];
+ this.removeEvents();
+ },
+
+ applyArgs: function(args, methodName) {
+ if(this.ready) {
+ this[methodName].apply(this, args);
+ } else {
+ this.queuedData = this.queuedData || [];
+ this.queuedData.push(args);
+ }
+ },
+
+ _addData: function(trigger, data) {
+ this._processData(trigger, data, 'addData');
+ },
+
+ _setData: function(trigger, data) {
+ this._processData(trigger, data, 'setData');
+ },
+
+ _processData: function(trigger, data, methodName) {
+ for(var i = 0; i < this.hooks.length; i++) {
+ var hook = this.hooks[i];
+ if(hook.trigger.dataset.hasOwnProperty('id')) {
+ if(hook.trigger.dataset.id === trigger) {
+ hook.list[methodName](data);
+ }
+ }
+ }
+ },
+
+ addEvents: function() {
+ var self = this;
+ this.windowClickedWrapper = function(e){
+ var thisTag = e.target;
+ if(thisTag.tagName !== 'UL'){
+ // climb up the tree to find the UL
+ thisTag = utils.closest(thisTag, 'UL');
+ }
+ if(utils.isDropDownParts(thisTag)){ return }
+ if(utils.isDropDownParts(e.target)){ return }
+ for(var i = 0; i < self.hooks.length; i++) {
+ self.hooks[i].list.hide();
+ }
+ }.bind(this);
+ document.addEventListener('click', this.windowClickedWrapper);
+ },
+
+ removeEvents: function(){
+ w.removeEventListener('click', this.windowClickedWrapper);
+ w.removeEventListener('load', this.loadWrapper);
+ },
+
+ changeHookList: function(trigger, list, plugins, config) {
+ trigger = document.querySelector('[data-id="'+trigger+'"]');
+ // list = document.querySelector(list);
+ this.hooks.every(function(hook, i) {
+ if(hook.trigger === trigger) {
+ hook.destroy();
+ this.hooks.splice(i, 1);
+ this.addHook(trigger, list, plugins, config);
+ return false;
+ }
+ return true
+ }.bind(this));
+ },
+
+ addHook: function(hook, list, plugins, config) {
+ if(!(hook instanceof HTMLElement) && typeof hook === 'string'){
+ hook = document.querySelector(hook);
+ }
+ if(!list){
+ list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]);
+ }
+
+ if(hook) {
+ if(hook.tagName === 'A' || hook.tagName === 'BUTTON') {
+ this.hooks.push(new HookButton(hook, list, plugins, config));
+ } else if(hook.tagName === 'INPUT') {
+ this.hooks.push(new HookInput(hook, list, plugins, config));
+ }
+ }
+ return this;
+ },
+
+ addHooks: function(hooks, plugins, config) {
+ for(var i = 0; i < hooks.length; i++) {
+ var hook = hooks[i];
+ this.addHook(hook, null, plugins, config);
+ }
+ return this;
+ },
+
+ setConfig: function(obj){
+ this.config = obj;
+ },
+
+ init: function () {
+ this.addEvents();
+ var readyEvent = new CustomEvent('ready.dl', {
+ detail: {
+ dropdown: this,
+ },
+ });
+ window.dispatchEvent(readyEvent);
+ this.ready = true;
+ for(var i = 0; i < this.queuedData.length; i++) {
+ this.addData.apply(this, this.queuedData[i]);
+ }
+ this.queuedData = [];
+ return this;
+ },
+ });
+
+ return DropLab;
+ };
+});
+
+},{"./constants":1,"./custom_event_polyfill":2,"./hook_button":6,"./hook_input":7,"./utils":10,"./window":11}],5:[function(require,module,exports){
+var DropDown = require('./dropdown');
+
+var Hook = function(trigger, list, plugins, config){
+ this.trigger = trigger;
+ this.list = new DropDown(list);
+ this.type = 'Hook';
+ this.event = 'click';
+ this.plugins = plugins || [];
+ this.config = config || {};
+ this.id = trigger.dataset.id;
+};
+
+Object.assign(Hook.prototype, {
+
+ addEvents: function(){},
+
+ constructor: Hook,
+});
+
+module.exports = Hook;
+
+},{"./dropdown":3}],6:[function(require,module,exports){
+var CustomEvent = require('./custom_event_polyfill');
+var Hook = require('./hook');
+
+var HookButton = function(trigger, list, plugins, config) {
+ Hook.call(this, trigger, list, plugins, config);
+ this.type = 'button';
+ this.event = 'click';
+ this.addEvents();
+ this.addPlugins();
+};
+
+HookButton.prototype = Object.create(Hook.prototype);
+
+Object.assign(HookButton.prototype, {
+ addPlugins: function() {
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].init(this);
+ }
+ },
+
+ clicked: function(e){
+ var buttonEvent = new CustomEvent('click.dl', {
+ detail: {
+ hook: this,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ this.list.show();
+ e.target.dispatchEvent(buttonEvent);
+ },
+
+ addEvents: function(){
+ this.clickedWrapper = this.clicked.bind(this);
+ this.trigger.addEventListener('click', this.clickedWrapper);
+ },
+
+ removeEvents: function(){
+ this.trigger.removeEventListener('click', this.clickedWrapper);
+ },
+
+ restoreInitialState: function() {
+ this.list.list.innerHTML = this.list.initialState;
+ },
+
+ removePlugins: function() {
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].destroy();
+ }
+ },
+
+ destroy: function() {
+ this.restoreInitialState();
+ this.removeEvents();
+ this.removePlugins();
+ },
+
+
+ constructor: HookButton,
+});
+
+
+module.exports = HookButton;
+
+},{"./custom_event_polyfill":2,"./hook":5}],7:[function(require,module,exports){
+var CustomEvent = require('./custom_event_polyfill');
+var Hook = require('./hook');
+
+var HookInput = function(trigger, list, plugins, config) {
+ Hook.call(this, trigger, list, plugins, config);
+ this.type = 'input';
+ this.event = 'input';
+ this.addPlugins();
+ this.addEvents();
+};
+
+Object.assign(HookInput.prototype, {
+ addPlugins: function() {
+ var self = this;
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].init(self);
+ }
+ },
+
+ addEvents: function(){
+ var self = this;
+
+ this.mousedown = function mousedown(e) {
+ if(self.hasRemovedEvents) return;
+
+ var mouseEvent = new CustomEvent('mousedown.dl', {
+ detail: {
+ hook: self,
+ text: e.target.value,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ e.target.dispatchEvent(mouseEvent);
+ }
+
+ this.input = function input(e) {
+ if(self.hasRemovedEvents) return;
+
+ var inputEvent = new CustomEvent('input.dl', {
+ detail: {
+ hook: self,
+ text: e.target.value,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ e.target.dispatchEvent(inputEvent);
+ self.list.show();
+ }
+
+ this.keyup = function keyup(e) {
+ if(self.hasRemovedEvents) return;
+
+ keyEvent(e, 'keyup.dl');
+ }
+
+ this.keydown = function keydown(e) {
+ if(self.hasRemovedEvents) return;
+
+ keyEvent(e, 'keydown.dl');
+ }
+
+ function keyEvent(e, keyEventName){
+ var keyEvent = new CustomEvent(keyEventName, {
+ detail: {
+ hook: self,
+ text: e.target.value,
+ which: e.which,
+ key: e.key,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ e.target.dispatchEvent(keyEvent);
+ self.list.show();
+ }
+
+ this.events = this.events || {};
+ this.events.mousedown = this.mousedown;
+ this.events.input = this.input;
+ this.events.keyup = this.keyup;
+ this.events.keydown = this.keydown;
+ this.trigger.addEventListener('mousedown', this.mousedown);
+ this.trigger.addEventListener('input', this.input);
+ this.trigger.addEventListener('keyup', this.keyup);
+ this.trigger.addEventListener('keydown', this.keydown);
+ },
+
+ removeEvents: function() {
+ this.hasRemovedEvents = true;
+ this.trigger.removeEventListener('mousedown', this.mousedown);
+ this.trigger.removeEventListener('input', this.input);
+ this.trigger.removeEventListener('keyup', this.keyup);
+ this.trigger.removeEventListener('keydown', this.keydown);
+ },
+
+ restoreInitialState: function() {
+ this.list.list.innerHTML = this.list.initialState;
+ },
+
+ removePlugins: function() {
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].destroy();
+ }
+ },
+
+ destroy: function() {
+ this.restoreInitialState();
+ this.removeEvents();
+ this.removePlugins();
+ this.list.destroy();
+ }
+});
+
+module.exports = HookInput;
+
+},{"./custom_event_polyfill":2,"./hook":5}],8:[function(require,module,exports){
+var DropLab = require('./droplab')();
+var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
+var keyboard = require('./keyboard')();
+var setup = function() {
+ window.DropLab = DropLab;
+};
+
+
+module.exports = setup();
+
+},{"./constants":1,"./droplab":4,"./keyboard":9}],9:[function(require,module,exports){
+require('./window')(function(w){
+ module.exports = function(){
+ var currentKey;
+ var currentFocus;
+ var currentIndex = 0;
+ var isUpArrow = false;
+ var isDownArrow = false;
+ var removeHighlight = function removeHighlight(list) {
+ var listItems = list.list.querySelectorAll('li');
+ for(var i = 0; i < listItems.length; i++) {
+ listItems[i].classList.remove('dropdown-active');
+ }
+ return listItems;
+ };
+
+ var setMenuForArrows = function setMenuForArrows(list) {
+ var listItems = removeHighlight(list);
+ if(currentIndex>0){
+ if(!listItems[currentIndex-1]){
+ currentIndex = currentIndex-1;
+ }
+ listItems[currentIndex-1].classList.add('dropdown-active');
+ }
+ };
+
+ var mousedown = function mousedown(e) {
+ var list = e.detail.hook.list;
+ removeHighlight(list);
+ list.show();
+ currentIndex = 0;
+ isUpArrow = false;
+ isDownArrow = false;
+ };
+ var selectItem = function selectItem(list) {
+ var listItems = removeHighlight(list);
+ var currentItem = listItems[currentIndex-1];
+ var listEvent = new CustomEvent('click.dl', {
+ detail: {
+ list: list,
+ selected: currentItem,
+ data: currentItem.dataset,
+ },
+ });
+ list.list.dispatchEvent(listEvent);
+ list.hide();
+ }
+
+ var keydown = function keydown(e){
+ var typedOn = e.target;
+ isUpArrow = false;
+ isDownArrow = false;
+
+ if(e.detail.which){
+ currentKey = e.detail.which;
+ if(currentKey === 13){
+ selectItem(e.detail.hook.list);
+ return;
+ }
+ if(currentKey === 38) {
+ isUpArrow = true;
+ }
+ if(currentKey === 40) {
+ isDownArrow = true;
+ }
+ } else if(e.detail.key) {
+ currentKey = e.detail.key;
+ if(currentKey === 'Enter'){
+ selectItem(e.detail.hook.list);
+ return;
+ }
+ if(currentKey === 'ArrowUp') {
+ isUpArrow = true;
+ }
+ if(currentKey === 'ArrowDown') {
+ isDownArrow = true;
+ }
+ }
+ if(isUpArrow){ currentIndex--; }
+ if(isDownArrow){ currentIndex++; }
+ if(currentIndex < 0){ currentIndex = 0; }
+ setMenuForArrows(e.detail.hook.list);
+ };
+
+ w.addEventListener('mousedown.dl', mousedown);
+ w.addEventListener('keydown.dl', keydown);
+ };
+});
+},{"./window":11}],10:[function(require,module,exports){
+var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
+var DATA_DROPDOWN = require('./constants').DATA_DROPDOWN;
+
+var toDataCamelCase = function(attr){
+ return this.camelize(attr.split('-').slice(1).join(' '));
+};
+
+// the tiniest damn templating I can do
+var t = function(s,d){
+ for(var p in d)
+ s=s.replace(new RegExp('{{'+p+'}}','g'), d[p]);
+ return s;
+};
+
+var camelize = function(str) {
+ return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) {
+ return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
+ }).replace(/\s+/g, '');
+};
+
+var closest = function(thisTag, stopTag) {
+ while(thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){
+ thisTag = thisTag.parentNode;
+ }
+ return thisTag;
+};
+
+var isDropDownParts = function(target) {
+ if(!target || target.tagName === 'HTML') { return false; }
+ return (
+ target.hasAttribute(DATA_TRIGGER) ||
+ target.hasAttribute(DATA_DROPDOWN)
+ );
+};
+
+module.exports = {
+ toDataCamelCase: toDataCamelCase,
+ t: t,
+ camelize: camelize,
+ closest: closest,
+ isDropDownParts: isDropDownParts,
+};
+
+},{"./constants":1}],11:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[8])(8)
+});
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
new file mode 100644
index 00000000000..f20610b3811
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -0,0 +1,79 @@
+/* eslint-disable */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* global droplab */
+
+require('../window')(function(w){
+ function droplabAjaxException(message) {
+ this.message = message;
+ }
+
+ w.droplabAjax = {
+ _loadUrlData: function _loadUrlData(url) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest;
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = function () {
+ if(xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ var data = JSON.parse(xhr.responseText);
+ return resolve(data);
+ } else {
+ return reject([xhr.responseText, xhr.status]);
+ }
+ }
+ };
+ xhr.send();
+ });
+ },
+
+ init: function init(hook) {
+ var self = this;
+ var config = hook.config.droplabAjax;
+
+ if (!config || !config.endpoint || !config.method) {
+ return;
+ }
+
+ if (config.method !== 'setData' && config.method !== 'addData') {
+ return;
+ }
+
+ if (config.loadingTemplate) {
+ var dynamicList = hook.list.list.querySelector('[data-dynamic]');
+
+ var loadingTemplate = document.createElement('div');
+ loadingTemplate.innerHTML = config.loadingTemplate;
+ loadingTemplate.setAttribute('data-loading-template', '');
+
+ this.listTemplate = dynamicList.outerHTML;
+ dynamicList.outerHTML = loadingTemplate.outerHTML;
+ }
+
+ this._loadUrlData(config.endpoint)
+ .then(function(d) {
+ if (config.loadingTemplate) {
+ var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]');
+
+ if (dataLoadingTemplate) {
+ dataLoadingTemplate.outerHTML = self.listTemplate;
+ }
+ }
+ hook.list[config.method].call(hook.list, d);
+ }).catch(function(e) {
+ throw new droplabAjaxException(e.message || e);
+ });
+ },
+
+ destroy: function() {
+ }
+ };
+});
+},{"../window":2}],2:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[1])(1)
+}); \ No newline at end of file
diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js
new file mode 100644
index 00000000000..af163f76851
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js
@@ -0,0 +1,145 @@
+/* eslint-disable */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* global droplab */
+
+require('../window')(function(w){
+ w.droplabAjaxFilter = {
+ init: function(hook) {
+ this.destroyed = false;
+ this.hook = hook;
+ this.notLoading();
+
+ this.debounceTriggerWrapper = this.debounceTrigger.bind(this);
+ this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper);
+ this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper);
+ this.trigger(true);
+ },
+
+ notLoading: function notLoading() {
+ this.loading = false;
+ },
+
+ debounceTrigger: function debounceTrigger(e) {
+ var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93];
+ var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1;
+ var focusEvent = e.type === 'focus';
+
+ if (invalidKeyPressed || this.loading) {
+ return;
+ }
+
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+
+ this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200);
+ },
+
+ trigger: function trigger(getEntireList) {
+ var config = this.hook.config.droplabAjaxFilter;
+ var searchValue = this.trigger.value;
+
+ if (!config || !config.endpoint || !config.searchKey) {
+ return;
+ }
+
+ if (config.searchValueFunction) {
+ searchValue = config.searchValueFunction();
+ }
+
+ if (config.loadingTemplate && this.hook.list.data === undefined ||
+ this.hook.list.data.length === 0) {
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+
+ var loadingTemplate = document.createElement('div');
+ loadingTemplate.innerHTML = config.loadingTemplate;
+ loadingTemplate.setAttribute('data-loading-template', true);
+
+ this.listTemplate = dynamicList.outerHTML;
+ dynamicList.outerHTML = loadingTemplate.outerHTML;
+ }
+
+ if (getEntireList) {
+ searchValue = '';
+ }
+
+ if (config.searchKey === searchValue) {
+ return this.list.show();
+ }
+
+ this.loading = true;
+
+ var params = config.params || {};
+ params[config.searchKey] = searchValue;
+ var self = this;
+ this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) {
+ if (config.loadingTemplate && self.hook.list.data === undefined ||
+ self.hook.list.data.length === 0) {
+ const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
+
+ if (dataLoadingTemplate) {
+ dataLoadingTemplate.outerHTML = self.listTemplate;
+ }
+ }
+
+ if (!self.destroyed) {
+ var hookListChildren = self.hook.list.list.children;
+ var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
+
+ if (onlyDynamicList && data.length === 0) {
+ self.hook.list.hide();
+ }
+
+ self.hook.list.setData.call(self.hook.list, data);
+ }
+ self.notLoading();
+ });
+ },
+
+ _loadUrlData: function _loadUrlData(url) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest;
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = function () {
+ if(xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ var data = JSON.parse(xhr.responseText);
+ return resolve(data);
+ } else {
+ return reject([xhr.responseText, xhr.status]);
+ }
+ }
+ };
+ xhr.send();
+ });
+ },
+
+ buildParams: function(params) {
+ if (!params) return '';
+ var paramsArray = Object.keys(params).map(function(param) {
+ return param + '=' + (params[param] || '');
+ });
+ return '?' + paramsArray.join('&');
+ },
+
+ destroy: function destroy() {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+
+ this.destroyed = true;
+
+ this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper);
+ this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper);
+ }
+ };
+});
+},{"../window":2}],2:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[1])(1)
+}); \ No newline at end of file
diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js
new file mode 100644
index 00000000000..41a220831f9
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab_filter.js
@@ -0,0 +1,60 @@
+/* eslint-disable */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* global droplab */
+
+require('../window')(function(w){
+ w.droplabFilter = {
+
+ keydownWrapper: function(e){
+ var list = e.detail.hook.list;
+ var data = list.data;
+ var value = e.detail.hook.trigger.value.toLowerCase();
+ var config = e.detail.hook.config.droplabFilter;
+ var matches = [];
+ var filterFunction;
+ // will only work on dynamically set data
+ if(!data){
+ return;
+ }
+
+ if (config && config.filterFunction && typeof config.filterFunction === 'function') {
+ filterFunction = config.filterFunction;
+ } else {
+ filterFunction = function(o){
+ // cheap string search
+ o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1;
+ return o;
+ };
+ }
+
+ matches = data.map(function(o) {
+ return filterFunction(o, value);
+ });
+ list.render(matches);
+ },
+
+ init: function init(hookInput) {
+ var config = hookInput.config.droplabFilter;
+
+ if (!config || (!config.template && !config.filterFunction)) {
+ return;
+ }
+
+ this.hookInput = hookInput;
+ this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
+ },
+
+ destroy: function destroy(){
+ this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
+ }
+ };
+});
+},{"../window":2}],2:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[1])(1)
+}); \ No newline at end of file
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 1a0aa9757ba..3d183f4ecb4 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */
+/* global Dropzone */
/*= require preview_markdown */
@@ -119,7 +120,7 @@
if (item.type.indexOf("image") !== -1) {
return item;
}
- i++;
+ i += 1;
}
return false;
};
@@ -214,7 +215,5 @@
}
return DropzoneInput;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index fd7f961aab9..d81d4cf8425 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* 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 } = {}) {
@@ -15,7 +16,7 @@
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.issueUpdateURL = $dropdown.data('issue-update');
this.rawSelectedDate = null;
this.displayedDate = null;
@@ -79,9 +80,12 @@
}
parseSelectedDate() {
- this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val();
+ this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
+
if (this.rawSelectedDate.length) {
- let dateObj = new Date(this.rawSelectedDate);
+ // 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';
@@ -131,7 +135,6 @@
return selectedDateValue.length ?
$('.js-remove-due-date-holder').removeClass('hidden') :
$('.js-remove-due-date-holder').addClass('hidden');
-
}
}).done((data) => {
if (isDropdown) {
@@ -145,25 +148,19 @@
class DueDateSelectors {
constructor() {
- this.initMilestoneDueDate();
+ this.initMilestoneDatePicker();
this.initIssuableSelect();
}
- initMilestoneDueDate() {
- const $datePicker = $('.datepicker');
+ initMilestoneDatePicker() {
+ $('.datepicker').datepicker({
+ dateFormat: 'yy-mm-dd'
+ });
- if ($datePicker.length) {
- const $dueDate = $('#milestone_due_date');
- $datePicker.datepicker({
- dateFormat: 'yy-mm-dd',
- onSelect: (dateText, inst) => {
- $dueDate.val(dateText);
- }
- }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
- }
- $('.js-clear-due-date').on('click', (e) => {
+ $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
e.preventDefault();
- $.datepicker._clearDate($datePicker);
+ const datepicker = $(e.target).siblings('.datepicker');
+ $.datepicker._clearDate(datepicker);
});
}
@@ -181,5 +178,4 @@
}
global.DueDateSelectors = DueDateSelectors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
new file mode 100644
index 00000000000..fea642467fa
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -0,0 +1,223 @@
+/* eslint-disable no-param-reassign, no-new */
+/* global Vue */
+/* global EnvironmentsService */
+/* global Flash */
+
+//= require vue
+//= require vue-resource
+//= require_tree ../services/
+//= 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: `
+ <div :class="cssContainerClass">
+ <div class="top-area">
+ <ul v-if="!isLoading" class="nav-links">
+ <li v-bind:class="{ 'active': scope === undefined }">
+ <a :href="projectEnvironmentsPath">
+ Available
+ <span class="badge js-available-environments-count">
+ {{state.availableCounter}}
+ </span>
+ </a>
+ </li><li v-bind:class="{ 'active' : scope === 'stopped' }">
+ <a :href="projectStoppedEnvironmentsPath">
+ Stopped
+ <span class="badge js-stopped-environments-count">
+ {{state.stoppedCounter}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
+ <a :href="newEnvironmentPath" class="btn btn-create">
+ New environment
+ </a>
+ </div>
+ </div>
+
+ <div class="environments-container">
+ <div class="environments-list-loading text-center" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.environments.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ You don't have any environments right now.
+ </h2>
+ <p class="blank-state-text">
+ Environments are places where code gets deployed, such as staging or production.
+ <br />
+ <a :href="helpPagePath">
+ Read more about environments
+ </a>
+ </p>
+
+ <a
+ v-if="canCreateEnvironmentParsed"
+ :href="newEnvironmentPath"
+ class="btn btn-create js-new-environment-button">
+ New Environment
+ </a>
+ </div>
+
+ <div class="table-holder"
+ v-if="!isLoading && state.filteredEnvironments.length > 0">
+ <table class="table ci-table environments">
+ <thead>
+ <tr>
+ <th class="environments-name">Environment</th>
+ <th class="environments-deploy">Last deployment</th>
+ <th class="environments-build">Build</th>
+ <th class="environments-commit">Commit</th>
+ <th class="environments-date">Created</th>
+ <th class="hidden-xs environments-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in state.filteredEnvironments"
+ v-bind:model="model">
+
+ <tr
+ is="environment-item"
+ :model="model"
+ :toggleRow="toggleRow.bind(model)"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :play-icon-svg="playIconSvg"
+ :terminal-icon-svg="terminalIconSvg"
+ :commit-icon-svg="commitIconSvg"></tr>
+
+ <tr v-if="model.isOpen && model.children && model.children.length > 0"
+ is="environment-item"
+ v-for="children in model.children"
+ :model="children"
+ :toggleRow="toggleRow.bind(children)"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :play-icon-svg="playIconSvg"
+ :terminal-icon-svg="terminalIconSvg"
+ :commit-icon-svg="commitIconSvg">
+ </tr>
+
+ </template>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6
new file mode 100644
index 00000000000..81468f4d3bc
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_actions.js.es6
@@ -0,0 +1,49 @@
+/*= require vue */
+/* global 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: `
+ <div class="inline">
+ <div class="dropdown">
+ <a class="dropdown-new btn btn-default" data-toggle="dropdown">
+ <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
+ <i class="fa fa-caret-down"></i>
+ </a>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <a :href="action.play_path"
+ data-method="post"
+ rel="nofollow"
+ class="js-manual-action-link">
+
+ <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
+
+ <span>
+ {{action.name}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6
new file mode 100644
index 00000000000..6592c1b5f0f
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6
@@ -0,0 +1,22 @@
+/*= require vue */
+/* global 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: `
+ <a class="btn external_url" :href="externalUrl" target="_blank">
+ <i class="fa fa-external-link"></i>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
new file mode 100644
index 00000000000..0e6bc3fdb2c
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -0,0 +1,537 @@
+/* global Vue */
+/* global timeago */
+
+/*= require timeago */
+/*= require lib/utils/text_utility */
+/*= require vue_common_component/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 `stoppable?` key provided in the response.
+ *
+ * @returns {Boolean}
+ */
+ isStoppable() {
+ return this.model['stoppable?'];
+ },
+
+ /**
+ * 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.<Object>|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: `
+ <tr>
+ <td v-bind:class="{ 'children-row': isChildren}">
+ <a v-if="!isFolder"
+ class="environment-name"
+ :href="model.environment_path">
+ {{model.name}}
+ </a>
+ <span v-else v-on:click="toggleRow(model)" class="folder-name">
+ <span class="folder-icon">
+ <i v-show="model.isOpen" class="fa fa-caret-down"></i>
+ <i v-show="!model.isOpen" class="fa fa-caret-right"></i>
+ </span>
+
+ <span>
+ {{model.name}}
+ </span>
+
+ <span class="badge">
+ {{childrenCounter}}
+ </span>
+ </span>
+ </td>
+
+ <td class="deployment-column">
+ <span v-if="shouldRenderDeploymentID">
+ {{deploymentInternalId}}
+ </span>
+
+ <span v-if="!isFolder && deploymentHasUser">
+ by
+ <a :href="deploymentUser.web_url" class="js-deploy-user-container">
+ <img class="avatar has-tooltip s20"
+ :src="deploymentUser.avatar_url"
+ :alt="userImageAltDescription"
+ :title="deploymentUser.username" />
+ </a>
+ </span>
+ </td>
+
+ <td class="environments-build-cell">
+ <a v-if="shouldRenderBuildName"
+ class="build-link"
+ :href="model.last_deployment.deployable.build_path">
+ {{buildName}}
+ </a>
+ </td>
+
+ <td>
+ <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component">
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"
+ :commit-icon-svg="commitIconSvg">
+ </commit-component>
+ </div>
+ <p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title">
+ No deployments yet
+ </p>
+ </td>
+
+ <td>
+ <span
+ v-if="!isFolder && canShowDate"
+ class="environment-created-date-timeago">
+ {{createdDate}}
+ </span>
+ </td>
+
+ <td class="hidden-xs">
+ <div v-if="!isFolder">
+ <div v-if="hasManualActions && canCreateDeployment"
+ class="inline js-manual-actions-container">
+ <actions-component
+ :play-icon-svg="playIconSvg"
+ :actions="manualActions">
+ </actions-component>
+ </div>
+
+ <div v-if="model.external_url && canReadEnvironment"
+ class="inline js-external-url-container">
+ <external-url-component
+ :external-url="model.external_url">
+ </external-url-component>
+ </div>
+
+ <div v-if="isStoppable && canCreateDeployment"
+ class="inline js-stop-component-container">
+ <stop-component
+ :stop-url="model.stop_path">
+ </stop-component>
+ </div>
+
+ <div v-if="model.terminal_path"
+ class="inline js-terminal-button-container">
+ <terminal-button-component
+ :terminal-icon-svg="terminalIconSvg"
+ :terminal-path="model.terminal_path">
+ </terminal-button-component>
+ </div>
+
+ <div v-if="canRetry && canCreateDeployment"
+ class="inline js-rollback-component-container">
+ <rollback-component
+ :is-last-deployment="isLastDeployment"
+ :retry-url="retryUrl">
+ </rollback-component>
+ </div>
+ </div>
+ </td>
+ </tr>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6
new file mode 100644
index 00000000000..b52298b4a88
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6
@@ -0,0 +1,32 @@
+/*= require vue */
+/* global 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: `
+ <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
+ <span v-if="isLastDeployment">
+ Re-deploy
+ </span>
+ <span v-else>
+ Rollback
+ </span>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6
new file mode 100644
index 00000000000..0a29f2f36e9
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_stop.js.es6
@@ -0,0 +1,26 @@
+/*= require vue */
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ gl.environmentsList.StopComponent = Vue.component('stop-component', {
+ props: {
+ stopUrl: {
+ type: String,
+ default: '',
+ },
+ },
+
+ template: `
+ <a class="btn stop-env-link"
+ :href="stopUrl"
+ data-confirm="Are you sure you want to stop this environment?"
+ data-method="post"
+ rel="nofollow">
+ <i class="fa fa-stop stop-env-icon"></i>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
new file mode 100644
index 00000000000..050184ba497
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
@@ -0,0 +1,27 @@
+/*= require vue */
+/* global 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: `
+ <a class="btn terminal-button"
+ :href="terminalPath">
+ <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
new file mode 100644
index 00000000000..9f24a6a4f88
--- /dev/null
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -0,0 +1,23 @@
+//= require vue
+//= require_tree ./stores/
+//= require ./components/environment
+//= require ./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.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6
new file mode 100644
index 00000000000..575a45d9802
--- /dev/null
+++ b/app/assets/javascripts/environments/services/environments_service.js.es6
@@ -0,0 +1,24 @@
+/* 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.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6
new file mode 100644
index 00000000000..9b4090100da
--- /dev/null
+++ b/app/assets/javascripts/environments/stores/environments_store.js.es6
@@ -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/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
new file mode 100644
index 00000000000..406bdbc1c7d
--- /dev/null
+++ b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
@@ -0,0 +1,12 @@
+/* global Vue */
+Vue.http.interceptors.push((request, next) => {
+ Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+ next((response) => {
+ if (typeof response.data === 'string') {
+ response.data = JSON.parse(response.data); // eslint-disable-line
+ }
+
+ Vue.activeResources--; // eslint-disable-line
+ });
+});
diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js
deleted file mode 100644
index 4c9e219aa43..00000000000
--- a/app/assets/javascripts/extensions/array.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/* eslint-disable */
-Array.prototype.first = function() {
- return this[0];
-}
-
-Array.prototype.last = function() {
- return this[this.length-1];
-}
diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6
new file mode 100644
index 00000000000..cd401277689
--- /dev/null
+++ b/app/assets/javascripts/extensions/array.js.es6
@@ -0,0 +1,24 @@
+/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, max-len */
+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.es6 b/app/assets/javascripts/extensions/custom_event.js.es6
new file mode 100644
index 00000000000..abedae4c1c7
--- /dev/null
+++ b/app/assets/javascripts/extensions/custom_event.js.es6
@@ -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/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6
index afb2f0d6956..90ab79305a7 100644
--- a/app/assets/javascripts/extensions/element.js.es6
+++ b/app/assets/javascripts/extensions/element.js.es6
@@ -1,9 +1,20 @@
/* global Element */
-/* eslint-disable consistent-return, max-len */
+/* eslint-disable consistent-return, max-len, no-empty, func-names */
-Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector;
-
-Element.prototype.closest = function closest(selector, selectedElement = this) {
+Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) {
if (!selectedElement) return;
return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement);
};
+
+Element.prototype.matches = Element.prototype.matches ||
+ Element.prototype.matchesSelector ||
+ Element.prototype.mozMatchesSelector ||
+ Element.prototype.msMatchesSelector ||
+ Element.prototype.oMatchesSelector ||
+ Element.prototype.webkitMatchesSelector ||
+ function (s) {
+ const matches = (this.document || this.ownerDocument).querySelectorAll(s);
+ let i = matches.length - 1;
+ while (i >= 0 && matches.item(i) !== this) { i -= 1; }
+ return i > -1;
+ };
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
index 623a80b7053..d3b58b2707a 100644
--- a/app/assets/javascripts/extensions/jquery.js
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */
// Disable an element and add the 'disabled' Bootstrap class
(function() {
$.fn.extend({
@@ -13,5 +13,4 @@
return $(this).removeAttr('disabled').removeClass('disabled');
}
});
-
}).call(this);
diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6
new file mode 100644
index 00000000000..70a2d765abd
--- /dev/null
+++ b/app/assets/javascripts/extensions/object.js.es6
@@ -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/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 732136f1f2c..895a872568d 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,6 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
+/* global FilesCommentButton */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.FilesCommentButton = (function() {
var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
@@ -130,7 +132,6 @@
};
return FilesCommentButton;
-
})();
$.fn.filesCommentButton = function() {
@@ -143,5 +144,4 @@
}
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
new file mode 100644
index 00000000000..7d297b8eee8
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
@@ -0,0 +1,69 @@
+/*= require filtered_search/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: '&lt;@author&gt;',
+ }, {
+ icon: 'fa-user',
+ hint: 'assignee:',
+ tag: '&lt;@assignee&gt;',
+ }, {
+ icon: 'fa-clock-o',
+ hint: 'milestone:',
+ tag: '&lt;%milestone&gt;',
+ }, {
+ icon: 'fa-tag',
+ hint: 'label:',
+ tag: '&lt;~label&gt;',
+ }];
+
+ 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.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
new file mode 100644
index 00000000000..13cbec1be4a
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
@@ -0,0 +1,44 @@
+/*= require filtered_search/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.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
new file mode 100644
index 00000000000..7bf199d9274
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
@@ -0,0 +1,53 @@
+/*= require filtered_search/filtered_search_dropdown */
+
+/* global droplabAjaxFilter */
+
+(() => {
+ class DropdownUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ droplabAjaxFilter: {
+ endpoint: '/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);
+
+ return lastToken.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.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
new file mode 100644
index 00000000000..443ac222f70
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
@@ -0,0 +1,120 @@
+(() => {
+ 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();
+ value = value.replace(/"(.*?)"/g, str => str.slice(1).slice(0, -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
+ // This helps with matching the beginning & end of a token:key
+ inputValue = inputValue.replace(/"(.*?)"/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
new file mode 100644
index 00000000000..d188718c5f3
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -0,0 +1,7 @@
+ // This is a manifest file that'll be compiled into including all the files listed below.
+ // Add new JavaScript code in separate files in this directory and they'll automatically
+ // be included in the compiled file accessible from http://example.com/assets/application.js
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+ // the compiled file.
+ //
+ /*= require_tree . */
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
new file mode 100644
index 00000000000..859d6515531
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
@@ -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 = `<div class="filter-dropdown-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>`;
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ this.itemClickedWrapper = this.itemClicked.bind(this);
+ this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
+ }
+
+ 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.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
new file mode 100644
index 00000000000..00e1c28692f
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
@@ -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('page:fetch', this.cleanupWrapper);
+ }
+
+ cleanup() {
+ if (this.droplab) {
+ this.droplab.destroy();
+ this.droplab = null;
+ }
+
+ this.setupMapping();
+
+ document.removeEventListener('page:fetch', 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](<arguments>)`
+ 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.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
new file mode 100644
index 00000000000..ae19bb68360
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -0,0 +1,216 @@
+/* global Turbolinks */
+
+(() => {
+ 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('page:fetch', this.cleanupWrapper);
+ }
+ }
+
+ cleanup() {
+ this.unbindEvents();
+ document.removeEventListener('page:fetch', 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 === 13) {
+ e.preventDefault();
+
+ // 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) {
+ paths.push(`search=${encodeURIComponent(searchToken)}`);
+ }
+
+ Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`);
+ }
+
+ 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.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
new file mode 100644
index 00000000000..e46373024b6
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
@@ -0,0 +1,83 @@
+(() => {
+ 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 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 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 tokenKeys.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.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
new file mode 100644
index 00000000000..cf53845a48b
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
@@ -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/flash.js b/app/assets/javascripts/flash.js
index 46e272c3311..249fe23d4cb 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */
(function() {
this.Flash = (function() {
var hideFlash;
@@ -38,7 +38,5 @@
}
return Flash;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 824413bf20f..a1b7b442882 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -1,22 +1,36 @@
-/* eslint-disable */
+/* 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.GitLab == null) {
- window.GitLab = {};
+ if (window.gl == null) {
+ window.gl = {};
+ }
+
+ function sanitize(str) {
+ return str.replace(/<(?:.|\n)*?>/gm, '');
}
- GitLab.GfmAutoComplete = {
- dataLoading: false,
- dataLoaded: false,
+ window.gl.GfmAutoComplete = {
+ dataSources: {},
+ defaultLoadingData: ['loading'],
cachedData: {},
- dataSource: '',
+ isLoadingData: {},
+ atTypeMap: {
+ ':': 'emojis',
+ '@': 'members',
+ '#': 'issues',
+ '!': 'mergeRequests',
+ '~': 'labels',
+ '%': 'milestones',
+ '/': 'commands'
+ },
// Emoji
Emoji: {
template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
},
// Team Members
Members: {
- template: '<li>${username} <small>${title}</small></li>'
+ template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
},
Labels: {
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
@@ -30,93 +44,102 @@
template: '<li>${title}</li>'
},
Loading: {
- template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+ template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
},
DefaultOptions: {
sorter: function(query, items, searchKey) {
- if ((items[0].name != null) && items[0].name === 'loading') {
+ 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 (data[0] === 'loading') {
+ if (gl.GfmAutoComplete.isLoading(data)) {
+ gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
return data;
+ } else {
+ return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
}
- return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
},
beforeInsert: function(value) {
- if (!GitLab.GfmAutoComplete.dataLoaded) {
- return this.at;
+ 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_\'\.\+\-]*)$", 'gi');
+
+ match = regexp.exec(subtext);
+
+ if (match) {
+ return match[2] || match[1];
} else {
- return value;
+ return null;
}
}
},
- setup: _.debounce(function(input) {
+ setup: function(input) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
- // destroy previous instances
- this.destroyAtWho();
- // set up instances
- this.setupAtWho();
-
- if (this.dataSource && !this.dataLoading && !this.cachedData) {
- this.dataLoading = true;
- return this.fetchData(this.dataSource)
- .done((data) => {
- this.dataLoading = false;
- this.loadData(data);
- });
- };
-
- if (this.cachedData != null) {
- return this.loadData(this.cachedData);
- }
- }, 1000),
- setupAtWho: function() {
+ 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
- this.input.atwho({
+ $input.atwho({
at: ':',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.path != null) {
- return _this.Emoji.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
+ displayTpl: function(value) {
+ return value.path != null ? this.Emoji.template : this.Loading.template;
+ }.bind(this),
insertTpl: ':${name}:',
- data: ['loading'],
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
callbacks: {
sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ filter: this.DefaultOptions.filter
}
});
// Team Members
- this.input.atwho({
+ $input.atwho({
at: '@',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.username != null) {
- return _this.Members.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
+ displayTpl: function(value) {
+ return value.username != null ? this.Members.template : this.Loading.template;
+ }.bind(this),
insertTpl: '${atwho-at}${username}',
searchKey: 'search',
- data: ['loading'],
+ 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) {
- var title;
+ let title = '';
if (m.username == null) {
return m;
}
@@ -124,34 +147,35 @@
if (m.count) {
title += " (" + m.count + ")";
}
+
+ const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
+ const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
+ const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
+
return {
username: m.username,
- title: gl.utils.sanitize(title),
- search: gl.utils.sanitize(m.username + " " + m.name)
+ avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
+ title: sanitize(title),
+ search: sanitize(m.username + " " + m.name)
};
});
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '#',
alias: 'issues',
searchKey: 'search',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.title != null) {
- return _this.Issues.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
- data: ['loading'],
+ 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) {
@@ -159,29 +183,27 @@
}
return {
id: i.iid,
- title: gl.utils.sanitize(i.title),
+ title: sanitize(i.title),
search: i.iid + " " + i.title
};
});
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '%',
alias: 'milestones',
searchKey: 'search',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.title != null) {
- return _this.Milestones.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
- insertTpl: '${atwho-at}"${title}"',
- data: ['loading'],
+ 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) {
@@ -189,32 +211,27 @@
}
return {
id: m.iid,
- title: gl.utils.sanitize(m.title),
+ title: sanitize(m.title),
search: "" + m.title
};
});
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '!',
alias: 'mergerequests',
searchKey: 'search',
- displayTpl: (function(_this) {
- return function(value) {
- if (value.title != null) {
- return _this.Issues.template;
- } else {
- return _this.Loading.template;
- }
- };
- })(this),
- data: ['loading'],
+ 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) {
@@ -222,32 +239,40 @@
}
return {
id: m.iid,
- title: gl.utils.sanitize(m.title),
+ title: sanitize(m.title),
search: m.iid + " " + m.title
};
});
}
}
});
- this.input.atwho({
+ $input.atwho({
at: '~',
alias: 'labels',
searchKey: 'search',
- displayTpl: this.Labels.template,
+ 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 "\"" + (gl.utils.sanitize(title)) + "\"";
+ return "\"" + (sanitize(title)) + "\"";
} else {
- return gl.utils.sanitize(title);
+ return sanitize(title);
}
};
return $.map(merges, function(m) {
return {
- title: sanitizeLabelTitle(m.title),
+ title: sanitize(m.title),
color: m.color,
search: "" + m.title
};
@@ -256,11 +281,14 @@
}
});
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
- this.input.filter('[data-supports-slash-commands="true"]').atwho({
+ $input.filter('[data-supports-slash-commands="true"]').atwho({
at: '/',
alias: 'commands',
searchKey: 'search',
+ skipSpecialCharacterTest: true,
+ data: this.defaultLoadingData,
displayTpl: function(value) {
+ if (this.isLoading(value)) return this.Loading.template;
var tpl = '<li>/${name}';
if (value.aliases.length > 0) {
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
@@ -273,7 +301,7 @@
}
tpl += '</li>';
return _.template(tpl)(value);
- },
+ }.bind(this),
insertTpl: function(value) {
var tpl = "/${name} ";
var reference_prefix = null;
@@ -291,6 +319,7 @@
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) {
@@ -306,7 +335,7 @@
});
},
matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
- var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi
+ var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
var match = regexp.exec(subtext);
if (match) {
return match[1];
@@ -318,33 +347,29 @@
});
return;
},
- destroyAtWho: function() {
- return this.input.atwho('destroy');
- },
- fetchData: function(dataSource) {
- return $.getJSON(dataSource);
+ 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(data) {
- this.cachedData = data;
- this.dataLoaded = true;
- // load members
- this.input.atwho('load', '@', data.members);
- // load issues
- this.input.atwho('load', 'issues', data.issues);
- // load milestones
- this.input.atwho('load', 'milestones', data.milestones);
- // load merge requests
- this.input.atwho('load', 'mergerequests', data.mergerequests);
- // load emojis
- this.input.atwho('load', ':', data.emojis);
- // load labels
- this.input.atwho('load', '~', data.labels);
- // load commands
- this.input.atwho('load', '/', data.commands);
+ 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 $(':focus').trigger('keyup');
+ return $input.trigger('keyup');
+ },
+ isLoading(data) {
+ if (!data || !data.length) return false;
+ if (Array.isArray(data)) data = data[0];
+ return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 98e43c4d088..7746535d9ed 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,8 +1,11 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
+/* global fuzzaldrinPlus */
+/* global Turbolinks */
+
(function() {
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
- bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
- indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+ bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
GitLabDropdownFilter = (function() {
var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
@@ -20,7 +23,6 @@
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
- this.indeterminateIds = [];
$clearButton.on('click', (function(_this) {
// Clear click
return function(e) {
@@ -35,7 +37,7 @@
.on('keydown', function (e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault()
+ e.preventDefault();
}
})
.on('input', function() {
@@ -131,7 +133,6 @@
};
return GitLabDropdownFilter;
-
})();
GitLabDropdownRemote = (function() {
@@ -184,11 +185,10 @@
};
return GitLabDropdownRemote;
-
})();
GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
+ var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
LOADING_CLASS = "is-loading";
@@ -204,7 +204,7 @@
SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
- CURSOR_SELECT_SCROLL_PADDING = 5
+ CURSOR_SELECT_SCROLL_PADDING = 5;
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
@@ -221,7 +221,7 @@
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
- this.highlight = !!this.options.highlight
+ this.highlight = !!this.options.highlight;
this.filterInputBlur = this.options.filterInputBlur != null
? this.options.filterInputBlur
: true;
@@ -249,7 +249,7 @@
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
- if (_this.options.filterable && _this.filter && _this.filter.input) {
+ if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
};
@@ -341,16 +341,18 @@
selector = ".dropdown-page-one .dropdown-content a";
}
this.dropdown.on("click", selector, function(e) {
- var $el, selected;
+ var $el, selected, selectedObj, isMarking;
$el = $(this);
selected = self.rowClicked($el);
+ selectedObj = selected ? selected[0] : null;
+ isMarking = selected ? selected[1] : null;
if (self.options.clicked) {
- self.options.clicked(selected, $el, e);
+ self.options.clicked(selectedObj, $el, e, isMarking);
}
// Update label right after all modifications in dropdown has been done
if (self.options.toggleLabel) {
- self.updateLabel(selected, $el, self);
+ self.updateLabel(selectedObj, $el, self);
}
$el.trigger('blur');
@@ -441,12 +443,6 @@
this.resetRows();
this.addArrowKeyEvent();
- if (this.options.setIndeterminateIds) {
- this.options.setIndeterminateIds.call(this);
- }
- if (this.options.setActiveIds) {
- this.options.setActiveIds.call(this);
- }
// Makes indeterminate items effective
if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
this.parseData(this.fullData);
@@ -478,12 +474,7 @@
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) {
- $input.blur().val("");
- }
- // Triggering 'keyup' will re-render the dropdown which is not always required
- // specially if we want to keep the state of the dropdown needed for bulk-assignment
- if (!this.options.persistWhenHide) {
- $input.trigger("input");
+ $input.blur();
}
if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
@@ -501,7 +492,7 @@
} else {
var ul = document.createElement('ul');
- for (var i = 0; i < html.length; i++) {
+ for (var i = 0; i < html.length; i += 1) {
var el = html[i];
if (el instanceof jQuery) {
@@ -557,7 +548,7 @@
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
- if (value) { value = value.toString().replace(/'/g, '\\\'') };
+ if (value) { value = value.toString().replace(/'/g, '\\\''); }
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
@@ -617,7 +608,8 @@
};
GitLabDropdown.prototype.rowClicked = function(el) {
- var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
+ var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
+
fieldName = this.options.fieldName;
isInput = $(this.el).is('input');
if (this.renderedData) {
@@ -638,7 +630,7 @@
el.addClass(ACTIVE_CLASS);
}
- return selectedObject;
+ return [selectedObject];
}
field = [];
@@ -647,10 +639,16 @@
: selectedObject.id;
if (isInput) {
field = $(this.el);
- } else if(value) {
+ } else if (value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
+
+ if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
+ return;
+ }
+
if (el.hasClass(ACTIVE_CLASS)) {
+ isMarking = false;
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
if (isInput) {
@@ -660,6 +658,7 @@
}
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
+ isMarking = true;
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
if (field && field.length && value == null) {
@@ -669,6 +668,7 @@
this.addInput(fieldName, value, selectedObject);
}
} else {
+ isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
if (!isInput) {
@@ -689,12 +689,12 @@
}
}
- return selectedObject;
+ return [selectedObject, isMarking];
};
GitLabDropdown.prototype.focusTextInput = function() {
- if (this.options.filterable) { this.filterInput.focus() }
- }
+ if (this.options.filterable) { this.filterInput.focus(); }
+ };
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
var $input;
@@ -800,7 +800,7 @@
listItemBottom = listItemTop + listItemHeight;
if (!index) {
// Scroll the dropdown content to the top
- $dropdownContent.scrollTop(0)
+ $dropdownContent.scrollTop(0);
} else if (index === ($listItems.length - 1)) {
// Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
@@ -827,7 +827,6 @@
};
return GitLabDropdown;
-
})();
$.fn.glDropdown = function(opts) {
@@ -837,5 +836,4 @@
}
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
index 6ce392d2a5b..16be930a2f4 100644
--- a/app/assets/javascripts/gl_field_errors.js.es6
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
//= require gl_field_error
@@ -45,5 +45,4 @@
}
global.GlFieldErrors = GlFieldErrors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index ce54c34492d..08b2494f3df 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
+/* global GitLab */
+/* global DropzoneInput */
+/* global autosize */
+
(function() {
this.GLForm = (function() {
function GLForm(form) {
@@ -26,13 +30,13 @@
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'));
- GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+ 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);
}
+ gl.text.init(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
return this.form.show();
@@ -54,7 +58,5 @@
};
return GLForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index e103748d499..32c26349da0 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
index b796a9abb49..2e6da5750de 100644
--- a/app/assets/javascripts/graphs/stat_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */
(function() {
this.StatGraph = (function() {
function StatGraph() {}
@@ -14,7 +14,5 @@
};
return StatGraph;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index 818bff0c413..73715286c4a 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,4 +1,9 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */
+/* global ContributorsGraph */
+/* global ContributorsAuthorGraph */
+/* global ContributorsMasterGraph */
+/* global ContributorsStatGraphUtil */
+/* global d3 */
/*= require d3 */
@@ -107,7 +112,5 @@
};
return ContributorsStatGraph;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index dea26a3f1e1..cacfc177fc8 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,9 +1,11 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return */
+/* global d3 */
+/* global ContributorsGraph */
/*= require d3 */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
@@ -89,7 +91,6 @@
};
return ContributorsGraph;
-
})();
this.ContributorsMasterGraph = (function(superClass) {
@@ -194,7 +195,6 @@
};
return ContributorsMasterGraph;
-
})(ContributorsGraph);
this.ContributorsAuthorGraph = (function(superClass) {
@@ -272,7 +272,5 @@
};
return ContributorsAuthorGraph;
-
})(ContributorsGraph);
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 362a77e868f..29c3163328f 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -1,17 +1,18 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
(function() {
window.ContributorsStatGraphUtil = {
parse_log: function(log) {
- var by_author, by_email, data, entry, i, len, total;
+ var by_author, by_email, data, entry, i, len, total, normalized_email;
total = {};
by_author = {};
by_email = {};
- for (i = 0, len = log.length; i < len; i++) {
+ for (i = 0, len = log.length; i < len; i += 1) {
entry = log[i];
if (total[entry.date] == null) {
this.add_date(entry.date, total);
}
- data = by_author[entry.author_name] || by_email[entry.author_email];
+ normalized_email = entry.author_email.toLowerCase();
+ data = by_author[entry.author_name] || by_email[normalized_email];
if (data == null) {
data = this.add_author(entry, by_author, by_email);
}
@@ -32,12 +33,14 @@
return collection[date].date = date;
},
add_author: function(author, by_author, by_email) {
- var data;
+ var data, normalized_email;
data = {};
data.author_name = author.author_name;
data.author_email = author.author_email;
+ normalized_email = author.author_email.toLowerCase();
by_author[author.author_name] = data;
- return by_email[author.author_email] = data;
+ by_email[normalized_email] = data;
+ return data;
},
store_data: function(entry, total, by_author) {
this.store_commits(total, by_author);
@@ -132,5 +135,4 @@
}
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index 774477dc7a9..10dfd05fe3c 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,13 +1,13 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
(function() {
this.GroupAvatar = (function() {
function GroupAvatar() {
- $('.js-choose-group-avatar-button').bind("click", function() {
+ $('.js-choose-group-avatar-button').on("click", function() {
var form;
form = $(this).closest("form");
return form.find(".js-group-avatar-input").click();
});
- $('.js-group-avatar-input').bind("change", function() {
+ $('.js-group-avatar-input').on("change", function() {
var filename, form;
form = $(this).closest("form");
filename = $(this).val().replace(/^.*[\\\/]/, '');
@@ -16,7 +16,5 @@
}
return GroupAvatar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6
new file mode 100644
index 00000000000..15e695e81cf
--- /dev/null
+++ b/app/assets/javascripts/group_label_subscription.js.es6
@@ -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/groups_select.js b/app/assets/javascripts/groups_select.js
index e3c39c895ba..a50bc4a9057 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */
+/* global Api */
+
(function() {
var slice = [].slice;
@@ -14,7 +16,7 @@
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
- options = { all_available: all_available, skip_groups: skip_groups };
+ var options = { all_available: all_available, skip_groups: skip_groups };
return Api.groups(query.term, options, function(groups) {
var data;
data = {
@@ -65,7 +67,5 @@
};
return GroupsSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 81fcaf06430..fa85f9a6c86 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,10 +1,8 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, no-var, max-len */
(function() {
-
$(document).on('todo:toggle', function(e, count) {
var $todoPendingCount = $('.todos-pending-count');
$todoPendingCount.text(gl.text.addDelimiter(count));
$todoPendingCount.toggleClass('hidden', count === 0);
});
-
})();
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index c53f7c88aa2..9390136d3d8 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, vars-on-top, no-new, max-len */
+
(function() {
- this.ImporterStatus = (function() {
+ window.ImporterStatus = (function() {
function ImporterStatus(jobs_url, import_url) {
this.jobs_url = jobs_url;
this.import_url = import_url;
@@ -67,7 +68,6 @@
};
return ImporterStatus;
-
})();
$(function() {
@@ -75,7 +75,7 @@
var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
var importPath = $('.js-importer-status').data('import-path');
- new ImporterStatus(jobsImportPath, importPath);
+ new window.ImporterStatus(jobsImportPath, importPath);
}
});
}).call(this);
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 46503c290ae..f63d700fd65 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -1,10 +1,13 @@
-/* eslint-disable */
-(function() {
+/* 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 Turbolinks */
+
+((global) => {
var issuable_created;
issuable_created = false;
- this.Issuable = {
+ global.Issuable = {
init: function() {
Issuable.initTemplates();
Issuable.initSearch();
@@ -31,7 +34,6 @@
e.preventDefault();
debouncedExecSearch(e);
});
-
},
initSearchState: function($searchInput) {
const currentSearchVal = $searchInput.val();
@@ -108,7 +110,11 @@
filterResults: (function(_this) {
return function(form) {
var formAction, formData, issuesUrl;
- formData = form.serialize();
+ formData = form.serializeArray();
+ formData = formData.filter(function(data) {
+ return data.value !== '';
+ });
+ formData = $.param(formData);
formAction = form.attr('action');
issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
@@ -141,8 +147,11 @@
const $issuesOtherFilters = $('.issues-other-filters');
const $issuesBulkUpdate = $('.issues_bulk_update');
+ this.issuableBulkActions.willUpdateLabels = false;
+ this.issuableBulkActions.setOriginalDropdownData();
+
if ($checkedIssues.length > 0) {
- let ids = $.map($checkedIssues, function(value) {
+ const ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
});
$updateIssuesIds.val(ids);
@@ -152,7 +161,6 @@
$updateIssuesIds.val([]);
$issuesBulkUpdate.hide();
$issuesOtherFilters.show();
- this.issuableBulkActions.willUpdateLabels = false;
}
return true;
},
@@ -178,5 +186,4 @@
});
}
};
-
-}).call(this);
+})(window);
diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6
new file mode 100644
index 00000000000..7d0465aa8b4
--- /dev/null
+++ b/app/assets/javascripts/issuable/issuable_bundle.js.es6
@@ -0,0 +1 @@
+//= require ./time_tracking/time_tracking_bundle
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
new file mode 100644
index 00000000000..72433df2818
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
@@ -0,0 +1,42 @@
+/* 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: `
+ <div class='sidebar-collapsed-icon'>
+ <div v-html='stopwatchSvg'></div>
+ <div class='time-tracking-collapsed-summary'>
+ <div class='compare' v-if='showComparisonState'>
+ <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
+ </div>
+ <div class='estimate-only' v-if='showEstimateOnlyState'>
+ <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
+ </div>
+ <div class='spend-only' v-if='showSpentOnlyState'>
+ <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
+ </div>
+ <div class='no-tracking' v-if='showNoTimeTrackingState'>
+ <span class='no-value'>None</span>
+ </div>
+ </div>
+ </div>
+ `,
+ });
+})();
+
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
new file mode 100644
index 00000000000..6abbd5dd167
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
@@ -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: `
+ <div class='time-tracking-comparison-pane'>
+ <div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
+ :aria-valuenow='timeRemainingTooltip'
+ :title='timeRemainingTooltip'
+ :data-original-title='timeRemainingTooltip'
+ :class='timeRemainingStatusClass'>
+ <div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
+ <div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
+ </div>
+ <div class='compare-display-container'>
+ <div class='compare-display pull-left'>
+ <span class='compare-label'>Spent</span>
+ <span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
+ </div>
+ <div class='compare-display estimated pull-right'>
+ <span class='compare-label'>Est</span>
+ <span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ `,
+ });
+})();
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
new file mode 100644
index 00000000000..309e9f2f9ef
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6
@@ -0,0 +1,13 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-estimate-only-pane', {
+ name: 'time-tracking-estimate-only-pane',
+ props: ['timeEstimateHumanReadable'],
+ template: `
+ <div class='time-tracking-estimate-only-pane'>
+ <span class='bold'>Estimated:</span>
+ {{ timeEstimateHumanReadable }}
+ </div>
+ `,
+ });
+})();
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
new file mode 100644
index 00000000000..d7ced6d7151
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6
@@ -0,0 +1,24 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-help-state', {
+ name: 'time-tracking-help-state',
+ props: ['docsUrl'],
+ template: `
+ <div class='time-tracking-help-state'>
+ <div class='time-tracking-info'>
+ <h4>Track time with slash commands</h4>
+ <p>Slash commands can be used in the issues description and comment boxes.</p>
+ <p>
+ <code>/estimate</code>
+ will update the estimated time with the latest command.
+ </p>
+ <p>
+ <code>/spend</code>
+ will update the sum of the time spent.
+ </p>
+ <a class='btn btn-default learn-more-button' :href='docsUrl'>Learn more</a>
+ </div>
+ </div>
+ `,
+ });
+})();
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
new file mode 100644
index 00000000000..1d2ca643b5b
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6
@@ -0,0 +1,11 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-no-tracking-pane', {
+ name: 'time-tracking-no-tracking-pane',
+ template: `
+ <div class='time-tracking-no-tracking-pane'>
+ <span class='no-value'>No estimate or time spent</span>
+ </div>
+ `,
+ });
+})();
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
new file mode 100644
index 00000000000..ed283fec3c3
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6
@@ -0,0 +1,13 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-spent-only-pane', {
+ name: 'time-tracking-spent-only-pane',
+ props: ['timeSpentHumanReadable'],
+ template: `
+ <div class='time-tracking-spend-only-pane'>
+ <span class='bold'>Spent:</span>
+ {{ timeSpentHumanReadable }}
+ </div>
+ `,
+ });
+})();
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
new file mode 100644
index 00000000000..26563a7713b
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
@@ -0,0 +1,118 @@
+/* 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: `
+ <div class='time_tracker time-tracking-component-wrap' v-cloak>
+ <time-tracking-collapsed-state
+ :show-comparison-state='showComparisonState'
+ :show-help-state='showHelpState'
+ :show-spent-only-state='showSpentOnlyState'
+ :show-estimate-only-state='showEstimateOnlyState'
+ :time-spent-human-readable='timeSpentHumanReadable'
+ :time-estimate-human-readable='timeEstimateHumanReadable'
+ :stopwatch-svg='stopwatchSvg'>
+ </time-tracking-collapsed-state>
+ <div class='title hide-collapsed'>
+ Time tracking
+ <div class='help-button pull-right'
+ v-if='!showHelpState'
+ @click='toggleHelpState(true)'>
+ <i class='fa fa-question-circle'></i>
+ </div>
+ <div class='close-help-button pull-right'
+ v-if='showHelpState'
+ @click='toggleHelpState(false)'>
+ <i class='fa fa-close'></i>
+ </div>
+ </div>
+ <div class='time-tracking-content hide-collapsed'>
+ <time-tracking-estimate-only-pane
+ v-if='showEstimateOnlyState'
+ :time-estimate-human-readable='timeEstimateHumanReadable'>
+ </time-tracking-estimate-only-pane>
+ <time-tracking-spent-only-pane
+ v-if='showSpentOnlyState'
+ :time-spent-human-readable='timeSpentHumanReadable'>
+ </time-tracking-spent-only-pane>
+ <time-tracking-no-tracking-pane
+ v-if='showNoTimeTrackingState'>
+ </time-tracking-no-tracking-pane>
+ <time-tracking-comparison-pane
+ v-if='showComparisonState'
+ :time-estimate='timeEstimate'
+ :time-spent='timeSpent'
+ :time-spent-human-readable='timeSpentHumanReadable'
+ :time-estimate-human-readable='timeEstimateHumanReadable'>
+ </time-tracking-comparison-pane>
+ <transition name='help-state-toggle'>
+ <time-tracking-help-state
+ v-if='showHelpState'
+ :docs-url='docsUrl'>
+ </time-tracking-help-state>
+ </transition>
+ </div>
+ </div>
+ `,
+ });
+})();
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
new file mode 100644
index 00000000000..0b8da2b1f4f
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
@@ -0,0 +1,61 @@
+/* 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_context.js b/app/assets/javascripts/issuable_context.js
index fae49ee6144..9c53cdee58e 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
+/* global UsersSelect */
+
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
@@ -64,7 +66,5 @@
};
return IssuableContext;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 849b45756ee..293b856dc4d 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,6 +1,11 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
+/* global GitLab */
+/* global UsersSelect */
+/* global ZenMode */
+/* global Autosave */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.IssuableForm = (function() {
IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
@@ -14,7 +19,7 @@
this.renderWipExplanation = bind(this.renderWipExplanation, this);
this.resetAutosave = bind(this.resetAutosave, this);
this.handleSubmit = bind(this.handleSubmit, this);
- GitLab.GfmAutoComplete.setup();
+ gl.GfmAutoComplete.setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
@@ -46,7 +51,7 @@
IssuableForm.prototype.handleSubmit = function() {
var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null;
- if ((parseInt(fieldId) || 0) > 0) {
+ if ((parseInt(fieldId, 10) || 0) > 0) {
if (!confirm(this.issueMoveConfirmMsg)) {
return false;
}
@@ -145,7 +150,5 @@
};
return IssuableForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 67ace697936..081b0d8b0d7 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,11 +1,12 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
+/* global Flash */
/*= require flash */
/*= require jquery.waitforimages */
/*= require task_list */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Issue = (function() {
function Issue() {
@@ -138,22 +139,17 @@
return;
}
return $.getJSON($container.data('path')).error(function() {
- $container.find('.checking').hide();
$container.find('.unavailable').show();
return new Flash('Failed to check if a new branch can be created.', 'alert');
}).success(function(data) {
if (data.can_create_branch) {
- $container.find('.checking').hide();
$container.find('.available').show();
} else {
- $container.find('.checking').hide();
return $container.find('.unavailable').show();
}
});
};
return Issue;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index d7262e5eb74..1d6eff11403 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.IssueStatusSelect = (function() {
function IssueStatusSelect() {
@@ -30,7 +30,5 @@
}
return IssueStatusSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index 9697fb33566..c260ad03d47 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -1,10 +1,13 @@
-/* eslint-disable */
-((global) => {
+/* 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 } = {}) {
- this.container = container || $('.content'),
+ 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;
@@ -13,10 +16,6 @@
Issuable.initChecks();
}
- getElement(selector) {
- return this.container.find(selector);
- }
-
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}
@@ -70,10 +69,7 @@
getUnmarkedIndeterminedLabels() {
const result = [];
- const labelsToKeep = [];
-
- this.getElement('.labels-filter .is-indeterminate')
- .each((i, el) => labelsToKeep.push($(el).data('labelId')));
+ const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
@@ -103,48 +99,67 @@
}
};
if (this.willUpdateLabels) {
- this.getLabelsToApply().map(function(id) {
- return formData.update.add_label_ids.push(id);
- });
- this.getLabelsToRemove().map(function(id) {
- return formData.update.remove_label_ids.push(id);
- });
+ formData.update.add_label_ids = this.$labelDropdown.data('marked');
+ formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
}
return formData;
}
- getLabelsToApply() {
+ 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 = [];
- const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
- $labels.each(function(k, label) {
- if (label) {
- return labelIds.push(parseInt($(label).val()));
- }
+
+ this.getElement('.selected_issue:checked').each((i, el) => {
+ labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
- return labelIds;
+ 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);
+ }
- /**
- * Returns Label IDs that will be removed from issue selection
- * @return {Array} Array of labels IDs
- */
-
- getLabelsToRemove() {
- const result = [];
- const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
- const labelsToApply = this.getLabelsToApply();
- indeterminatedLabels.map(function(id) {
- // We need to exclude label IDs that will be applied
- // By not doing this will cause issues from selection to not add labels at all
- if (labelsToApply.indexOf(id) === -1) {
- return result.push(id);
- }
+ // 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);
});
- return result;
+ // 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.es6 b/app/assets/javascripts/label_manager.js.es6
index 175623e7448..8f48b1f57ce 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
-((global) => {
+/* 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');
@@ -102,6 +103,4 @@
}
gl.LabelManager = LabelManager;
-
})(window.gl || (window.gl = {}));
-
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index 3033e8ca5c2..40ad6fc348e 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -1,6 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Labels = (function() {
function Labels() {
@@ -42,7 +42,5 @@
};
return Labels;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index c334e3e0c02..fd1e229e30a 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,12 +1,16 @@
-/* eslint-disable */
+/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread */
+/* global Issuable */
+/* global ListLabel */
+
(function() {
this.LabelsSelect = (function() {
function LabelsSelect() {
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
$dropdown = $(dropdown);
+ $dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespace-path');
projectPath = $dropdown.data('project-path');
@@ -122,7 +126,7 @@
});
});
};
- return $dropdown.glDropdown({
+ $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
@@ -169,33 +173,40 @@
});
},
renderRow: function(label, instance) {
- var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing;
+ var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
removesAll = label.id <= 0 || (label.id == null);
if ($dropdown.hasClass('js-filter-bulk-update')) {
- indeterminate = instance.indeterminateIds;
- active = instance.activeIds;
+ indeterminate = $dropdown.data('indeterminate') || [];
+ marked = $dropdown.data('marked') || [];
+
if (indeterminate.indexOf(label.id) !== -1) {
selectedClass.push('is-indeterminate');
}
- if (active.indexOf(label.id) !== -1) {
+
+ if (marked.indexOf(label.id) !== -1) {
// Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
selectedClass.push('is-active');
- // Add input manually
- instance.addInput(this.fieldName, label.id);
}
- }
- if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) {
- selectedClass.push('is-active');
- }
- if ($dropdown.hasClass('js-multiselect') && removesAll) {
- selectedClass.push('dropdown-clear-active');
+ } else {
+ if (this.id(label)) {
+ dropdownName = $dropdown.data('fieldName');
+ dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
+
+ if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ selectedClass.push('is-active');
+ }
+ }
+
+ if ($dropdown.hasClass('js-multiselect') && removesAll) {
+ selectedClass.push('dropdown-clear-active');
+ }
}
if (label.duplicate) {
spacing = 100 / label.color.length;
@@ -231,7 +242,6 @@
// Return generated html
return $li.html($a).prop('outerHTML');
},
- persistWhenHide: $dropdown.data('persistWhenHide'),
search: {
fields: ['title']
},
@@ -310,32 +320,28 @@
}
}
}
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- // If we are persisting state we need the classes
- if (!this.options.persistWhenHide) {
- return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
- }
- }
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(label, $el, e) {
+ clicked: function(label, $el, e, isMarking) {
var isIssueIndex, isMRIndex, page;
- _this.enableBulkLabelDropdown();
+
+ page = $('body').data('page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown.parent()
.find('.dropdown-clear-active')
- .removeClass('is-active')
+ .removeClass('is-active');
}
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ _this.enableBulkLabelDropdown();
+ _this.setDropdownData($dropdown, isMarking, this.id(label));
return;
}
- page = $('body').data('page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
@@ -397,17 +403,10 @@
}
}
},
- setIndeterminateIds: function() {
- if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- return this.indeterminateIds = _this.getIndeterminateIds();
- }
- },
- setActiveIds: function() {
- if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
- return this.activeIds = _this.getActiveIds();
- }
- }
});
+
+ // Set dropdown data
+ _this.setOriginalDropdownData($dropdownContainer, $dropdown);
});
this.bindEvents();
}
@@ -420,34 +419,9 @@
if ($('.selected_issue:checked').length) {
return;
}
- // Remove inputs
- $('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
- // Also restore button text
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
};
- LabelsSelect.prototype.getIndeterminateIds = function() {
- var label_ids;
- label_ids = [];
- $('.selected_issue:checked').each(function(i, el) {
- var issue_id;
- issue_id = $(el).data('id');
- return label_ids.push($("#issue_" + issue_id).data('labels'));
- });
- return _.flatten(label_ids);
- };
-
- LabelsSelect.prototype.getActiveIds = function() {
- var label_ids;
- label_ids = [];
- $('.selected_issue:checked').each(function(i, el) {
- var issue_id;
- issue_id = $(el).data('id');
- return label_ids.push($("#issue_" + issue_id).data('labels'));
- });
- return _.intersection.apply(_, label_ids);
- };
-
LabelsSelect.prototype.enableBulkLabelDropdown = function() {
var issuableBulkActions;
if ($('.selected_issue:checked').length) {
@@ -456,8 +430,58 @@
}
};
- return LabelsSelect;
+ LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
+ var i, markedIds, unmarkedIds, indeterminateIds;
+ var issuableBulkActions = $('.bulk-update').data('bulkActions');
- })();
+ markedIds = $dropdown.data('marked') || [];
+ unmarkedIds = $dropdown.data('unmarked') || [];
+ indeterminateIds = $dropdown.data('indeterminate') || [];
+
+ if (isMarking) {
+ markedIds.push(value);
+
+ i = indeterminateIds.indexOf(value);
+ if (i > -1) {
+ indeterminateIds.splice(i, 1);
+ }
+
+ i = unmarkedIds.indexOf(value);
+ if (i > -1) {
+ unmarkedIds.splice(i, 1);
+ }
+ } else {
+ // If marked item (not common) is unmarked
+ i = markedIds.indexOf(value);
+ if (i > -1) {
+ markedIds.splice(i, 1);
+ }
+ // If an indeterminate item is being unmarked
+ if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+
+ // If a marked item is being unmarked
+ // (a marked item could also be a label that is present in all selection)
+ if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+ }
+
+ $dropdown.data('marked', markedIds);
+ $dropdown.data('unmarked', unmarkedIds);
+ $dropdown.data('indeterminate', indeterminateIds);
+ };
+
+ LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) {
+ var labels = [];
+ $container.find('[name="label_name[]"]').map(function() {
+ return labels.push(this.value);
+ });
+ $dropdown.data('marked', labels);
+ };
+
+ return LabelsSelect;
+ })();
}).call(this);
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 6b4edf02f4d..1c0ea317c1a 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
+
(function() {
var hideEndFade;
@@ -27,10 +28,10 @@
});
$scrollingTabs.each(function () {
- var $this = $(this),
- scrollingTabWidth = $this.width(),
- $active = $this.find('.active'),
- activeWidth = $active.width();
+ var $this = $(this);
+ var scrollingTabWidth = $this.width();
+ var $active = $this.find('.active');
+ var activeWidth = $active.width();
if ($active.length) {
var offset = $active.offset().left + activeWidth;
@@ -43,5 +44,4 @@
}
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
index b1718e89d3d..4cdf99cae72 100644
--- a/app/assets/javascripts/lib/ace.js
+++ b/app/assets/javascripts/lib/ace.js
@@ -1,3 +1,2 @@
-/* eslint-disable */
/*= require ace-rails-ap */
/*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
index e1dfdae97de..d8ad5aaeffe 100644
--- a/app/assets/javascripts/lib/chart.js
+++ b/app/assets/javascripts/lib/chart.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require Chart */
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
index 155e30cc462..5221f85ba7a 100644
--- a/app/assets/javascripts/lib/cropper.js
+++ b/app/assets/javascripts/lib/cropper.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require cropper */
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
index 0c9c2787077..57e7986576c 100644
--- a/app/assets/javascripts/lib/d3.js
+++ b/app/assets/javascripts/lib/d3.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require d3 */
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
index cc445db274b..5a9a501efe3 100644
--- a/app/assets/javascripts/lib/raphael.js
+++ b/app/assets/javascripts/lib/raphael.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require raphael */
/*= require g.raphael */
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
index a68edab2aad..ce090a2e4fd 100644
--- a/app/assets/javascripts/lib/utils/animate.js
+++ b/app/assets/javascripts/lib/utils/animate.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, max-len */
(function() {
(function(w) {
if (w.gl == null) {
@@ -46,5 +46,4 @@
return dfd.promise();
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
new file mode 100644
index 00000000000..e810ee85bd3
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
@@ -0,0 +1,113 @@
+/**
+ * Linked Tabs
+ *
+ * Handles persisting and restores the current tab selection and content.
+ * Reusable component for static content.
+ *
+ * ### Example Markup
+ *
+ * <ul class="nav-links tab-links">
+ * <li class="active">
+ * <a data-action="tab1" data-target="#tab1" data-toggle="tab" href="/path/tab1">
+ * Tab 1
+ * </a>
+ * </li>
+ * <li class="groups-tab">
+ * <a data-action="tab2" data-target="#tab2" data-toggle="tab" href="/path/tab2">
+ * Tab 2
+ * </a>
+ * </li>
+ *
+ *
+ * <div class="tab-content">
+ * <div class="tab-pane" id="tab1">
+ * Tab 1 Content
+ * </div>
+ * <div class="tab-pane" id="tab2">
+ * Tab 2 Content
+ * </div>
+ * </div>
+ *
+ *
+ * ### How to use
+ *
+ * new window.gl.LinkedTabs({
+ * action: "#{controller.action_name}",
+ * defaultAction: 'tab1',
+ * parentEl: '.tab-links'
+ * });
+ */
+
+(() => {
+ window.gl = window.gl || {};
+
+ window.gl.LinkedTabs = class LinkedTabs {
+ /**
+ * Binds the events and activates de default tab.
+ *
+ * @param {Object} options
+ */
+ constructor(options) {
+ this.options = options || {};
+
+ this.defaultAction = this.options.defaultAction;
+ this.action = this.options.action || this.defaultAction;
+
+ if (this.action === 'show') {
+ this.action = this.defaultAction;
+ }
+
+ this.currentLocation = window.location;
+
+ const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
+
+ // since this is a custom event we need jQuery :(
+ $(document)
+ .off('shown.bs.tab', tabSelector)
+ .on('shown.bs.tab', tabSelector, e => this.tabShown(e));
+
+ this.activateTab(this.action);
+ }
+
+ /**
+ * Handles the `shown.bs.tab` event to set the currect url action.
+ *
+ * @param {type} evt
+ * @return {Function}
+ */
+ tabShown(evt) {
+ const source = evt.target.getAttribute('href');
+
+ return this.setCurrentAction(source);
+ }
+
+ /**
+ * Updates the URL with the path that matched the given action.
+ *
+ * @param {String} source
+ * @return {String}
+ */
+ setCurrentAction(source) {
+ const copySource = source;
+
+ copySource.replace(/\/+$/, '');
+
+ const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
+
+ history.replaceState({
+ turbolinks: true,
+ url: newState,
+ }, document.title, newState);
+ return newState;
+ }
+
+ /**
+ * Given the current action activates the correct tab.
+ * http://getbootstrap.com/javascript/#tab-show
+ * Note: Will trigger `shown.bs.tab`
+ */
+ activateTab() {
+ return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
+ }
+ };
+})();
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
deleted file mode 100644
index 6cb3d95f984..00000000000
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/* eslint-disable */
-(function() {
- (function(w) {
- var base;
- w.gl || (w.gl = {});
- (base = w.gl).utils || (base.utils = {});
- w.gl.utils.isInGroupsPage = function() {
- return gl.utils.getPagePath() === 'groups';
- };
- w.gl.utils.isInProjectPage = function() {
- return gl.utils.getPagePath() === 'projects';
- };
- w.gl.utils.getProjectSlug = function() {
- if (this.isInProjectPage()) {
- return $('body').data('project');
- } else {
- return null;
- }
- };
- w.gl.utils.getGroupSlug = function() {
- if (this.isInGroupsPage()) {
- return $('body').data('group');
- } else {
- return null;
- }
- };
-
- w.gl.utils.ajaxGet = function(url) {
- return $.ajax({
- type: "GET",
- url: url,
- dataType: "script"
- });
- };
-
- w.gl.utils.split = function(val) {
- return val.split(/,\s*/);
- };
-
- w.gl.utils.extractLast = function(term) {
- return this.split(term).pop();
- };
-
- w.gl.utils.rstrip = function rstrip(val) {
- if (val) {
- return val.replace(/\s+$/, '');
- } else {
- return val;
- }
- };
-
- w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
- event_name = event_name || 'input';
- var closest_submit, field, that;
- that = this;
- field = $(field_selector);
- closest_submit = field.closest('form').find(button_selector);
- if (this.rstrip(field.val()) === "") {
- closest_submit.disable();
- }
- return field.on(event_name, function() {
- if (that.rstrip($(this).val()) === "") {
- return closest_submit.disable();
- } else {
- return closest_submit.enable();
- }
- });
- };
-
- w.gl.utils.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) {
- var closest_submit, updateButtons;
- closest_submit = form.find(button_selector);
- updateButtons = function() {
- var filled;
- filled = true;
- form.find('input').filter(form_selector).each(function() {
- return filled = this.rstrip($(this).val()) !== "" || !$(this).attr('required');
- });
- if (filled) {
- return closest_submit.enable();
- } else {
- return closest_submit.disable();
- }
- };
- updateButtons();
- return form.keyup(updateButtons);
- };
-
- w.gl.utils.sanitize = function(str) {
- return str.replace(/<(?:.|\n)*?>/gm, '');
- };
-
- w.gl.utils.unbindEvents = function() {
- return $(document).off('scroll');
- };
-
- w.gl.utils.shiftWindow = function() {
- return w.scrollBy(0, -100);
- };
-
-
- gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
- return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle');
- };
- gl.utils.preventDisabledButtons = function() {
- return $('.btn').click(function(e) {
- if ($(this).hasClass('disabled')) {
- e.preventDefault();
- e.stopImmediatePropagation();
- return false;
- }
- });
- };
- gl.utils.getPagePath = function() {
- return $('body').data('page').split(':')[0];
- };
- gl.utils.parseUrl = function (url) {
- var parser = document.createElement('a');
- parser.href = url;
- return parser;
- };
- gl.utils.cleanupBeforeFetch = function() {
- // Unbind scroll events
- $(document).off('scroll');
- // Close any open tooltips
- $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
- };
- })(window);
-
-}).call(this);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
new file mode 100644
index 00000000000..6d57d31f380
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -0,0 +1,163 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */
+(function() {
+ (function(w) {
+ var base;
+ w.gl || (w.gl = {});
+ (base = w.gl).utils || (base.utils = {});
+ w.gl.utils.isInGroupsPage = function() {
+ return gl.utils.getPagePath() === 'groups';
+ };
+ w.gl.utils.isInProjectPage = function() {
+ return gl.utils.getPagePath() === 'projects';
+ };
+ w.gl.utils.getProjectSlug = function() {
+ if (this.isInProjectPage()) {
+ return $('body').data('project');
+ } else {
+ return null;
+ }
+ };
+ w.gl.utils.getGroupSlug = function() {
+ if (this.isInGroupsPage()) {
+ return $('body').data('group');
+ } else {
+ return null;
+ }
+ };
+
+ w.gl.utils.ajaxGet = function(url) {
+ return $.ajax({
+ type: "GET",
+ url: url,
+ dataType: "script"
+ });
+ };
+
+ w.gl.utils.extractLast = function(term) {
+ return this.split(term).pop();
+ };
+
+ w.gl.utils.rstrip = function rstrip(val) {
+ if (val) {
+ return val.replace(/\s+$/, '');
+ } else {
+ return val;
+ }
+ };
+
+ w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
+ event_name = event_name || 'input';
+ var closest_submit, field, that;
+ that = this;
+ field = $(field_selector);
+ closest_submit = field.closest('form').find(button_selector);
+ if (this.rstrip(field.val()) === "") {
+ closest_submit.disable();
+ }
+ return field.on(event_name, function() {
+ if (that.rstrip($(this).val()) === "") {
+ return closest_submit.disable();
+ } else {
+ return closest_submit.enable();
+ }
+ });
+ };
+
+ // automatically adjust scroll position for hash urls taking the height of the navbar into account
+ // https://github.com/twitter/bootstrap/issues/1768
+ w.gl.utils.handleLocationHash = function() {
+ var hash = w.gl.utils.getLocationHash();
+ if (!hash) return;
+
+ var navbar = document.querySelector('.navbar-gitlab');
+ var subnav = document.querySelector('.layout-nav');
+ var fixedTabs = document.querySelector('.js-tabs-affix');
+
+ var adjustment = 0;
+ if (navbar) adjustment -= navbar.offsetHeight;
+ if (subnav) adjustment -= subnav.offsetHeight;
+
+ // scroll to user-generated markdown anchor if we cannot find a match
+ if (document.getElementById(hash) === null) {
+ var target = document.getElementById('user-content-' + hash);
+ if (target && target.scrollIntoView) {
+ target.scrollIntoView(true);
+ window.scrollBy(0, adjustment);
+ }
+ } else {
+ // only adjust for fixedTabs when not targeting user-generated content
+ if (fixedTabs) {
+ adjustment -= fixedTabs.offsetHeight;
+ }
+ window.scrollBy(0, adjustment);
+ }
+ };
+
+ // Check if element scrolled into viewport from above or below
+ // Courtesy http://stackoverflow.com/a/7557433/414749
+ w.gl.utils.isInViewport = function(el) {
+ var rect = el.getBoundingClientRect();
+
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= window.innerHeight &&
+ rect.right <= window.innerWidth
+ );
+ };
+
+ gl.utils.getPagePath = function(index) {
+ index = index || 0;
+ return $('body').data('page').split(':')[index];
+ };
+
+ gl.utils.parseUrl = function (url) {
+ var parser = document.createElement('a');
+ parser.href = url;
+ return parser;
+ };
+
+ gl.utils.parseUrlPathname = function (url) {
+ var parsedUrl = gl.utils.parseUrl(url);
+ // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
+ // We have to make sure we always have an absolute path.
+ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
+ };
+
+ gl.utils.getUrlParamsArray = function () {
+ // We can trust that each param has one & since values containing & will be encoded
+ // Remove the first character of search as it is always ?
+ return window.location.search.slice(1).split('&');
+ };
+
+ gl.utils.isMetaKey = function(e) {
+ return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
+ };
+
+ gl.utils.scrollToElement = function($el) {
+ var top = $el.offset().top;
+ gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
+ gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height();
+ gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
+
+ return $('body, html').animate({
+ scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + gl.mrTabsHeight)
+ }, 200);
+ };
+
+ /**
+ this will take in the `name` of the param you want to parse in the url
+ if the name does not exist this function will return `null`
+ otherwise it will return the value of the param key provided
+ */
+ w.gl.utils.getParameterByName = (name) => {
+ const url = window.location.href;
+ name = name.replace(/[[\]]/g, '\\$&');
+ const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
+ const results = regex.exec(url);
+ if (!results) return null;
+ if (!results[2]) return '';
+ return decodeURIComponent(results[2].replace(/\+/g, ' '));
+ };
+ })(window);
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 3965109dd65..3ed8bfd5651 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,4 +1,10 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
+/* global timeago */
+/* global dateFormat */
+
+/*= require timeago */
+/*= require date.format */
+
(function() {
(function(w) {
var base;
@@ -23,7 +29,7 @@
setTimeago = true;
}
- $timeagoEls.each(function() {
+ $timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
var $el = $(this);
$el.attr('title', gl.utils.formatDate($el.attr('datetime')));
@@ -33,6 +39,8 @@
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
}
+
+ $el.attr('data-timeago-rendered', true);
gl.utils.renderTimeago($el);
});
};
@@ -89,7 +97,5 @@
return Math.floor((date2 - date1) / millisecondsPerDay);
};
-
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index dafc006d2e5..6d5979603b9 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */
+
(function() {
(function(w) {
var notificationGranted, notifyMe, notifyPermissions;
@@ -43,5 +44,4 @@
w.notify = notifyMe;
return w.notifyPermissions = notifyPermissions;
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js.es6
new file mode 100644
index 00000000000..ae397212e55
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/pretty_time.js.es6
@@ -0,0 +1,65 @@
+(() => {
+ /*
+ * TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints,
+ * stringifyTime condensed or non-condensed, abbreviateTimelengths)
+ * */
+
+ const utils = window.gl.utils = gl.utils || {};
+ const prettyTime = utils.prettyTime = {
+ /*
+ * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
+ * Seconds can be negative or positive, zero or non-zero.
+ */
+ parseSeconds(seconds) {
+ const DAYS_PER_WEEK = 5;
+ const HOURS_PER_DAY = 8;
+ const MINUTES_PER_HOUR = 60;
+ const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
+ const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
+
+ const timePeriodConstraints = {
+ weeks: MINUTES_PER_WEEK,
+ days: MINUTES_PER_DAY,
+ hours: MINUTES_PER_HOUR,
+ minutes: 1,
+ };
+
+ let unorderedMinutes = prettyTime.secondsToMinutes(seconds);
+
+ return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
+ const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
+
+ unorderedMinutes -= (periodCount * minutesPerPeriod);
+
+ return periodCount;
+ });
+ },
+
+ /*
+ * Accepts a timeObject and returns a condensed string representation of it
+ * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
+ */
+
+ stringifyTime(timeObject) {
+ const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
+ const isNonZero = !!unitValue;
+ return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
+ }, '').trim();
+ return reducedTime.length ? reducedTime : '0m';
+ },
+
+ /*
+ * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
+ * the first non-zero unit/value pair.
+ */
+
+ abbreviateTime(timeStr) {
+ return timeStr.split(' ')
+ .filter(unitStr => unitStr.charAt(0) !== '0')[0];
+ },
+
+ secondsToMinutes(seconds) {
+ return Math.abs(seconds / 60);
+ },
+ };
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 98f9815ff05..6bb575059b7 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */
+
(function() {
(function(w) {
var base;
@@ -10,13 +11,28 @@
}
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
- }
+ };
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
gl.text.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
+ gl.text.getTextWidth = function(text, font) {
+ /**
+ * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
+ *
+ * @param {String} text The text to be rendered.
+ * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
+ *
+ * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
+ */
+ // re-use canvas object for better performance
+ var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
+ var context = canvas.getContext('2d');
+ context.font = font;
+ return context.measureText(text).width;
+ };
gl.text.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
@@ -44,9 +60,25 @@
}
};
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
- var insertText, inserted, selectedSplit, startChar;
+ var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine;
+ removedLastNewLine = false;
+ removedFirstNewLine = false;
+
+ // Remove the first newline
+ if (selected.indexOf('\n') === 0) {
+ removedFirstNewLine = true;
+ selected = selected.replace(/\n+/, '');
+ }
+
+ // Remove the last newline
+ if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
+
selectedSplit = selected.split('\n');
startChar = !wrap && textArea.selectionStart > 0 ? '\n' : '';
+
if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
if (blockTag != null) {
insertText = this.blockTagText(text, textArea, blockTag, selected);
@@ -62,6 +94,15 @@
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
+
+ if (removedFirstNewLine) {
+ insertText = '\n' + insertText;
+ }
+
+ if (removedLastNewLine) {
+ insertText += '\n';
+ }
+
if (document.queryCommandSupported('insertText')) {
inserted = document.execCommand('insertText', false, insertText);
}
@@ -74,9 +115,9 @@
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
- return this.moveCursor(textArea, tag, wrap);
+ return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
- gl.text.moveCursor = function(textArea, tag, wrapped) {
+ gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
@@ -87,6 +128,11 @@
} else {
pos = textArea.selectionStart;
}
+
+ if (removedLastNewLine) {
+ pos -= 1;
+ }
+
return textArea.setSelectionRange(pos, pos);
}
};
@@ -112,9 +158,11 @@
gl.text.removeListeners = function(form) {
return $('.js-md', form).off();
};
+ gl.text.humanize = function(string) {
+ return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
+ };
return gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/timeago.js b/app/assets/javascripts/lib/utils/timeago.js
deleted file mode 100644
index 42606dd2d46..00000000000
--- a/app/assets/javascripts/lib/utils/timeago.js
+++ /dev/null
@@ -1,237 +0,0 @@
-/**
- * Copyright (c) 2016 hustcc
- * License: MIT
- * Version: v2.0.2
- * https://github.com/hustcc/timeago.js
- * This is a forked from (https://gitlab.com/ClemMakesApps/timeago.js)
-**/
-/* eslint-disable */
-/* jshint expr: true */
-!function (root, factory) {
- if (typeof module === 'object' && module.exports)
- module.exports = factory(root);
- else
- root.timeago = factory(root);
-}(typeof window !== 'undefined' ? window : this,
-function () {
- var cnt = 0, // the timer counter, for timer key
- indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'),
-
- // build-in locales: en & zh_CN
- locales = {
- 'en': function(number, index) {
- if (index === 0) return ['just now', 'right now'];
- var unit = indexMapEn[parseInt(index / 2)];
- if (number > 1) unit += 's';
- return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit];
- },
- },
- // second, minute, hour, day, week, month, year(365 days)
- SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12],
- SEC_ARRAY_LEN = 6,
- ATTR_DATETIME = 'datetime';
-
- // format Date / string / timestamp to Date instance.
- function toDate(input) {
- if (input instanceof Date) return input;
- if (!isNaN(input)) return new Date(toInt(input));
- if (/^\d+$/.test(input)) return new Date(toInt(input, 10));
- input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds
- .replace(/-/, '/').replace(/-/, '/')
- .replace(/T/, ' ').replace(/Z/, ' UTC')
- .replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400
- return new Date(input);
- }
- // change f into int, remove Decimal. just for code compression
- function toInt(f) {
- return parseInt(f);
- }
- // format the diff second to *** time ago, with setting locale
- function formatDiff(diff, locale, defaultLocale) {
- // if locale is not exist, use defaultLocale.
- // if defaultLocale is not exist, use build-in `en`.
- // be sure of no error when locale is not exist.
- locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en');
- // if (! locales[locale]) locale = defaultLocale;
- var i = 0;
- agoin = diff < 0 ? 1 : 0; // timein or timeago
- diff = Math.abs(diff);
-
- for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
- diff /= SEC_ARRAY[i];
- }
- diff = toInt(diff);
- i *= 2;
-
- if (diff > (i === 0 ? 9 : 1)) i += 1;
- return locales[locale](diff, i)[agoin].replace('%s', diff);
- }
- // calculate the diff second between date to be formated an now date.
- function diffSec(date, nowDate) {
- nowDate = nowDate ? toDate(nowDate) : new Date();
- return (nowDate - toDate(date)) / 1000;
- }
- /**
- * nextInterval: calculate the next interval time.
- * - diff: the diff sec between now and date to be formated.
- *
- * What's the meaning?
- * diff = 61 then return 59
- * diff = 3601 (an hour + 1 second), then return 3599
- * make the interval with high performace.
- **/
- function nextInterval(diff) {
- var rst = 1, i = 0, d = Math.abs(diff);
- for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
- diff /= SEC_ARRAY[i];
- rst *= SEC_ARRAY[i];
- }
- // return leftSec(d, rst);
- d = d % rst;
- d = d ? rst - d : rst;
- return Math.ceil(d);
- }
- // get the datetime attribute, jQuery and DOM
- function getDateAttr(node) {
- if (node.getAttribute) return node.getAttribute(ATTR_DATETIME);
- if(node.attr) return node.attr(ATTR_DATETIME);
- }
- /**
- * timeago: the function to get `timeago` instance.
- * - nowDate: the relative date, default is new Date().
- * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
- *
- * How to use it?
- * var timeagoLib = require('timeago.js');
- * var timeago = timeagoLib(); // all use default.
- * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
- * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
- * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
- **/
- function Timeago(nowDate, defaultLocale) {
- var timers = {}; // real-time render timers
- // if do not set the defaultLocale, set it with `en`
- if (! defaultLocale) defaultLocale = 'en'; // use default build-in locale
- // what the timer will do
- function doRender(node, date, locale, cnt) {
- var diff = diffSec(date, nowDate);
- node.innerHTML = formatDiff(diff, locale, defaultLocale);
- // waiting %s seconds, do the next render
- timers['k' + cnt] = setTimeout(function() {
- doRender(node, date, locale, cnt);
- }, nextInterval(diff) * 1000);
- }
- /**
- * nextInterval: calculate the next interval time.
- * - diff: the diff sec between now and date to be formated.
- *
- * What's the meaning?
- * diff = 61 then return 59
- * diff = 3601 (an hour + 1 second), then return 3599
- * make the interval with high performace.
- **/
- // this.nextInterval = function(diff) { // for dev test
- // var rst = 1, i = 0, d = Math.abs(diff);
- // for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
- // diff /= SEC_ARRAY[i];
- // rst *= SEC_ARRAY[i];
- // }
- // // return leftSec(d, rst);
- // d = d % rst;
- // d = d ? rst - d : rst;
- // return Math.ceil(d);
- // }; // for dev test
- /**
- * format: format the date to *** time ago, with setting or default locale
- * - date: the date / string / timestamp to be formated
- * - locale: the formated string's locale name, e.g. en / zh_CN
- *
- * How to use it?
- * var timeago = require('timeago.js')();
- * timeago.format(new Date(), 'pl'); // Date instance
- * timeago.format('2016-09-10', 'fr'); // formated date string
- * timeago.format(1473473400269); // timestamp with ms
- **/
- this.format = function(date, locale) {
- return formatDiff(diffSec(date, nowDate), locale, defaultLocale);
- };
- /**
- * render: render the DOM real-time.
- * - nodes: which nodes will be rendered.
- * - locale: the locale name used to format date.
- *
- * How to use it?
- * var timeago = new require('timeago.js')();
- * // 1. javascript selector
- * timeago.render(document.querySelectorAll('.need_to_be_rendered'));
- * // 2. use jQuery selector
- * timeago.render($('.need_to_be_rendered'), 'pl');
- *
- * Notice: please be sure the dom has attribute `datetime`.
- **/
- this.render = function(nodes, locale) {
- if (nodes.length === undefined) nodes = [nodes];
- for (var i = 0; i < nodes.length; i++) {
- doRender(nodes[i], getDateAttr(nodes[i]), locale, ++ cnt); // render item
- }
- };
- /**
- * cancel: cancel all the timers which are doing real-time render.
- *
- * How to use it?
- * var timeago = new require('timeago.js')();
- * timeago.render(document.querySelectorAll('.need_to_be_rendered'));
- * timeago.cancel(); // will stop all the timer, stop render in real time.
- **/
- this.cancel = function() {
- for (var key in timers) {
- clearTimeout(timers[key]);
- }
- timers = {};
- };
- /**
- * setLocale: set the default locale name.
- *
- * How to use it?
- * var timeago = require('timeago.js');
- * timeago = new timeago();
- * timeago.setLocale('fr');
- **/
- this.setLocale = function(locale) {
- defaultLocale = locale;
- };
- return this;
- }
- /**
- * timeago: the function to get `timeago` instance.
- * - nowDate: the relative date, default is new Date().
- * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
- *
- * How to use it?
- * var timeagoLib = require('timeago.js');
- * var timeago = timeagoLib(); // all use default.
- * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
- * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
- * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
- **/
- function timeagoFactory(nowDate, defaultLocale) {
- return new Timeago(nowDate, defaultLocale);
- }
- /**
- * register: register a new language locale
- * - locale: locale name, e.g. en / zh_CN, notice the standard.
- * - localeFunc: the locale process function
- *
- * How to use it?
- * var timeagoLib = require('timeago.js');
- *
- * timeagoLib.register('the locale name', the_locale_func);
- * // or
- * timeagoLib.register('pl', require('timeago.js/locales/pl'));
- **/
- timeagoFactory.register = function(locale, localeFunc) {
- locales[locale] = localeFunc;
- };
-
- return timeagoFactory;
-}); \ No newline at end of file
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index 4fd1e3fc1d3..6d813d61601 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-return-assign, max-len */
(function() {
(function(w) {
var base;
@@ -12,5 +12,4 @@
return (obj != null) && (obj.constructor === Object);
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 44a66a915e3..8e15bf0735c 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
(function() {
(function(w) {
var base;
@@ -22,7 +22,7 @@
if (sParameterName[0] === sParam) {
values.push(sParameterName[1].replace(/\+/g, ' '));
}
- i++;
+ i += 1;
}
return values;
};
@@ -57,7 +57,7 @@
return ((function() {
var j, len, results;
results = [];
- for (j = 0, len = urlVariables.length; j < len; j++) {
+ for (j = 0, len = urlVariables.length; j < len; j += 1) {
variables = urlVariables[j];
if (variables.indexOf(param) === -1) {
results.push(variables);
@@ -77,5 +77,4 @@
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6
new file mode 100644
index 00000000000..eff1dcabfa2
--- /dev/null
+++ b/app/assets/javascripts/lib/vue_resource.js.es6
@@ -0,0 +1,2 @@
+//= require vue
+//= require vue-resource
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index ea5a60bb78e..4620715a521 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
+
// LineHighlighter
//
// Handles single- and multi-line selection and highlight for blob views.
@@ -30,7 +31,7 @@
// </div>
//
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.LineHighlighter = (function() {
// CSS class applied to highlighted lines
@@ -118,11 +119,11 @@
// Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches;
- //?L(\d+)(?:-(\d+))?$/)
+ // ?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
- first = parseInt(matches[1]);
- last = matches[2] ? parseInt(matches[2]) : null;
+ first = parseInt(matches[1], 10);
+ last = matches[2] ? parseInt(matches[2], 10) : null;
return [first, last];
} else {
return [null, null];
@@ -143,7 +144,7 @@
var i, lineNumber, ref, ref1, results;
if (range[1]) {
results = [];
- for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? ++i : --i) {
+ for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
results.push(this.highlightLine(lineNumber));
}
return results;
@@ -177,7 +178,5 @@
};
return LineHighlighter;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index d4f86534f0c..ea9bfb4860a 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
+/* global Turbolinks */
+
(function() {
Turbolinks.enableProgressBar();
@@ -9,5 +11,4 @@
$(document).on('page:change', function() {
$('.tanuki-logo').removeClass('animate');
});
-
}).call(this);
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js.es6
index 0bd90c57396..bf6c0ec2798 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js.es6
@@ -1,30 +1,29 @@
-/* eslint-disable */
-(function() {
+(() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
// `js-clear-input` element, then show that element when there is a value in the
// datepicker, and make clicking on that element clear the field.
//
- gl.MemberExpirationDate = function() {
+ window.gl = window.gl || {};
+ gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
}
-
- var inputs = $('.js-access-expiration-date');
+ const inputs = $(selector);
inputs.datepicker({
dateFormat: 'yy-mm-dd',
minDate: 1,
- onSelect: function () {
+ onSelect: function onSelect() {
$(this).trigger('change');
toggleClearInput.call(this);
- }
+ },
});
- inputs.next('.js-clear-input').on('click', function(event) {
+ inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault();
- var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
+ const input = $(this).closest('.clearable-input').find(selector);
input.datepicker('setDate', null)
.trigger('change');
toggleClearInput.call(input);
diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6
index 895bc10784f..e3f367a11eb 100644
--- a/app/assets/javascripts/members.js.es6
+++ b/app/assets/javascripts/members.js.es6
@@ -1,38 +1,81 @@
-/* eslint-disable */
-((w) => {
- w.gl = w.gl || {};
+/* eslint-disable class-methods-use-this */
+(() => {
+ window.gl = window.gl || {};
class Members {
constructor() {
this.addListeners();
+ this.initGLDropdown();
}
addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
- $('.js-member-update-control').off('change').on('change', this.formSubmit);
- $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess);
+ $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
+ $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
+ initGLDropdown() {
+ $('.js-member-permissions-dropdown').each((i, btn) => {
+ const $btn = $(btn);
+
+ $btn.glDropdown({
+ selectable: true,
+ isSelectable(selected, $el) {
+ return !$el.hasClass('is-active');
+ },
+ fieldName: $btn.data('field-name'),
+ id(selected, $el) {
+ return $el.data('id');
+ },
+ toggleLabel(selected, $el) {
+ return $el.text();
+ },
+ clicked: (selected, $link) => {
+ this.formSubmit(null, $link);
+ },
+ });
+ });
+ }
+
removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
- .fadeOut(function () {
+ .fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
- formSubmit() {
- $(this).closest('form').trigger("submit.rails").end().disable();
+ formSubmit(e, $el = null) {
+ const $this = e ? $(e.currentTarget) : $el;
+ const { $toggle, $dateInput } = this.getMemberListItems($this);
+
+ $this.closest('form').trigger('submit.rails');
+
+ $toggle.disable();
+ $dateInput.disable();
}
- formSuccess() {
- $(this).find('.js-member-update-control').enable();
+ formSuccess(e) {
+ const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
+
+ $toggle.enable();
+ $dateInput.enable();
+ }
+
+ getMemberListItems($el) {
+ const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
+
+ return {
+ $memberListItem,
+ $toggle: $memberListItem.find('.dropdown-menu-toggle'),
+ $dateInput: $memberListItem.find('.js-access-expiration-date'),
+ };
}
}
gl.Members = Members;
-})(window);
+})();
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
index 6da3942ea52..c7e78fed8fe 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
@@ -1,6 +1,9 @@
-/* eslint-disable */
-((global) => {
+/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
+/* global Vue */
+/* global ace */
+/* global Flash */
+((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.diffFileEditor = Vue.extend({
@@ -15,7 +18,7 @@
loading: false,
fileLoaded: false,
originalContent: '',
- }
+ };
},
computed: {
classObject() {
@@ -36,7 +39,7 @@
this.loadEditor();
}
},
- ready() {
+ mounted() {
if (this.file.loadEditor) {
this.loadEditor();
}
@@ -47,8 +50,8 @@
$.get(this.file.content_path)
.done((file) => {
- let content = this.$el.querySelector('pre');
- let fileContent = document.createTextNode(file.content);
+ const content = this.$el.querySelector('pre');
+ const fileContent = document.createTextNode(file.content);
content.textContent = fileContent.textContent;
@@ -90,5 +93,4 @@
}
}
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
index 23c4618af70..240c8f98932 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
-((global) => {
+/* eslint-disable no-param-reassign, comma-dangle */
+/* global Vue */
+((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.inlineConflictLines = Vue.extend({
@@ -9,5 +10,4 @@
},
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
deleted file mode 100644
index 797850262cc..00000000000
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
+++ /dev/null
@@ -1,15 +0,0 @@
-/* eslint-disable */
-((global) => {
-
- global.mergeConflicts = global.mergeConflicts || {};
-
- global.mergeConflicts.parallelConflictLine = Vue.extend({
- props: {
- file: Object,
- line: Object
- },
- mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
- template: '#parallel-conflict-line'
- });
-
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
index 1b3e9901f1e..97753c50b60 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
@@ -1,16 +1,28 @@
-/* eslint-disable */
-((global) => {
+/* eslint-disable no-param-reassign, comma-dangle */
+/* global Vue */
+((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLines = Vue.extend({
props: {
file: Object
},
- mixins: [global.mergeConflicts.utils],
- components: {
- 'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine
- }
+ mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
+ template: `
+ <table>
+ <tr class="line_holder parallel" v-for="section in file.parallelLines">
+ <template v-for="line in section">
+ <td class="diff-line-num header" :class="lineCssClass(line)" v-if="line.isHeader"></td>
+ <td class="line_content header" :class="lineCssClass(line)" v-if="line.isHeader">
+ <strong>{{line.richText}}</strong>
+ <button class="btn" @click="handleSelected(file, line.id, line.section)">{{line.buttonTitle}}</button>
+ </td>
+ <td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td>
+ <td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td>
+ </template>
+ </tr>
+ </table>
+ `,
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
index 8a7519b0786..c012b77e0bf 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, comma-dangle */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -24,8 +25,7 @@
method: 'POST'
});
}
- };
+ }
global.mergeConflicts.mergeConflictsService = mergeConflictsService;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
index f94e51e783c..74587df22c5 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */
+/* global Cookies */
+/* global Vue */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -37,7 +40,6 @@
commitMessage: data.commit_message,
sourceBranch: data.source_branch,
targetBranch: data.target_branch,
- commitMessage: data.commit_message,
shortCommitSha: data.commit_sha.slice(0, 7),
};
},
@@ -86,7 +88,7 @@
this.decorateLineForInlineView(line, id, conflict);
file.inlineLines.push(line);
- })
+ });
if (conflict) {
file.inlineLines.push(this.getOriginHeaderLine(id));
@@ -118,7 +120,7 @@
} else {
const lineType = type || 'context';
- linesObj.left.push (this.getLineForParallelView(line, id, lineType));
+ linesObj.left.push(this.getLineForParallelView(line, id, lineType));
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
}
});
@@ -126,7 +128,7 @@
this.checkLineLengths(linesObj);
});
- for (let i = 0, len = linesObj.left.length; i < len; i++) {
+ for (let i = 0, len = linesObj.left.length; i < len; i += 1) {
file.parallelLines.push([
linesObj.right[i],
linesObj.left[i]
@@ -159,11 +161,11 @@
if (file.type === CONFLICT_TYPES.TEXT) {
file.sections.forEach((section) => {
if (section.conflict) {
- count++;
+ count += 1;
}
});
} else {
- count++;
+ count += 1;
}
});
@@ -249,17 +251,17 @@
},
checkLineLengths(linesObj) {
- let { left, right } = linesObj;
+ const { left, right } = linesObj;
if (left.length !== right.length) {
if (left.length > right.length) {
const diff = left.length - right.length;
- for (let i = 0; i < diff; i++) {
+ for (let i = 0; i < diff; i += 1) {
right.push({ lineType: 'emptyLine', richText: '' });
}
} else {
const diff = right.length - left.length;
- for (let i = 0; i < diff; i++) {
+ for (let i = 0; i < diff; i += 1) {
left.push({ lineType: 'emptyLine', richText: '' });
}
}
@@ -313,32 +315,31 @@
const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
let unresolved = 0;
- for (let i = 0, l = files.length; i < l; i++) {
- let file = files[i];
+ for (let i = 0, l = files.length; i < l; i += 1) {
+ const file = files[i];
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
let numberConflicts = 0;
- let resolvedConflicts = Object.keys(file.resolutionData).length
+ const resolvedConflicts = Object.keys(file.resolutionData).length;
// We only check for conflicts type 'text'
// since conflicts `text_editor` can´t be resolved in interactive mode
if (file.type === CONFLICT_TYPES.TEXT) {
- for (let j = 0, k = file.sections.length; j < k; j++) {
+ for (let j = 0, k = file.sections.length; j < k; j += 1) {
if (file.sections[j].conflict) {
- numberConflicts++;
+ numberConflicts += 1;
}
}
if (resolvedConflicts !== numberConflicts) {
- unresolved++;
+ unresolved += 1;
}
}
} else if (file.resolveMode === EDIT_RESOLVE_MODE) {
-
// Unlikely to happen since switching to Edit mode saves content automatically.
// Checking anyway in case the save strategy changes in the future
if (!file.content) {
- unresolved++;
+ unresolved += 1;
continue;
}
}
@@ -363,15 +364,12 @@
};
this.state.conflictsData.files.forEach((file) => {
- let addFile;
-
- addFile = {
+ const addFile = {
old_path: file.old_path,
new_path: file.new_path
};
if (file.type === CONFLICT_TYPES.TEXT) {
-
// Submit only one data for type of editing
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
addFile.sections = file.resolutionData;
@@ -432,5 +430,4 @@
return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT);
}
};
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
index 222a5dcfc2e..a2d90f9ba47 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable new-cap, comma-dangle, no-new */
+/* global Vue */
+/* global Flash */
+
//= require vue
//= require ./merge_conflict_store
//= require ./merge_conflict_service
@@ -6,7 +9,6 @@
//= require ./mixins/line_conflict_actions
//= require ./components/diff_file_editor
//= require ./components/inline_conflict_lines
-//= require ./components/parallel_conflict_line
//= require ./components/parallel_conflict_lines
$(() => {
@@ -27,10 +29,10 @@ $(() => {
'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
},
computed: {
- conflictsCountText() { return mergeConflictsStore.getConflictsCountText() },
- readyToCommit() { return mergeConflictsStore.isReadyToCommit() },
- commitButtonText() { return mergeConflictsStore.getCommitButtonText() },
- showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent() }
+ conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); },
+ readyToCommit() { return mergeConflictsStore.isReadyToCommit(); },
+ commitButtonText() { return mergeConflictsStore.getCommitButtonText(); },
+ showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
},
created() {
mergeConflictsService
@@ -49,7 +51,7 @@ $(() => {
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
- $(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight();
+ $('.js-syntax-highlight').syntaxHighlight();
});
});
},
@@ -86,5 +88,5 @@ $(() => {
});
}
}
- })
+ });
});
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
index c8de586aa21..53e000d7e9e 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, comma-dangle */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -9,5 +10,4 @@
}
}
};
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
index 88c3a20ce13..0f475f62ee6 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, quote-props, comma-dangle */
+
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -15,5 +16,4 @@
}
}
};
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index d3bd1e846c1..09ee8dbe9d7 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,11 +1,12 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */
+/* global MergeRequestTabs */
/*= require jquery.waitforimages */
/*= require task_list */
/*= require merge_request_tabs */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.MergeRequest = (function() {
function MergeRequest(opts) {
@@ -26,6 +27,7 @@
// Prevent duplicate event bindings
this.disableTaskList();
this.initMRBtnListeners();
+ this.initCommitMessageListeners();
if ($("a.btn-close").length) {
this.initTaskList();
}
@@ -40,7 +42,7 @@
if (window.mrTabs) {
window.mrTabs.unbindEvents();
}
- window.mrTabs = new MergeRequestTabs(this.opts);
+ window.mrTabs = new gl.MergeRequestTabs(this.opts);
};
MergeRequest.prototype.showAllCommits = function() {
@@ -107,8 +109,26 @@
// note so that we can re-use its form here
};
- return MergeRequest;
+ MergeRequest.prototype.initCommitMessageListeners = function() {
+ var textarea = $('textarea.js-commit-message');
- })();
+ $('a.js-with-description-link').on('click', function(e) {
+ e.preventDefault();
+
+ textarea.val(textarea.data('messageWithDescription'));
+ $('p.js-with-description-hint').hide();
+ $('p.js-without-description-hint').show();
+ });
+
+ $('a.js-without-description-link').on('click', function(e) {
+ e.preventDefault();
+ textarea.val(textarea.data('messageWithoutDescription'));
+ $('p.js-with-description-hint').show();
+ $('p.js-without-description-hint').hide();
+ });
+ };
+
+ return MergeRequest;
+ })();
}).call(this);
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
deleted file mode 100644
index 860ee5df57e..00000000000
--- a/app/assets/javascripts/merge_request_tabs.js
+++ /dev/null
@@ -1,441 +0,0 @@
-/* eslint-disable */
-// MergeRequestTabs
-//
-// Handles persisting and restoring the current tab selection and lazily-loading
-// content on the MergeRequests#show page.
-//
-/*= require js.cookie */
-
-//
-// ### Example Markup
-//
-// <ul class="nav-links merge-request-tabs">
-// <li class="notes-tab active">
-// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
-// Discussion
-// </a>
-// </li>
-// <li class="commits-tab">
-// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
-// Commits
-// </a>
-// </li>
-// <li class="diffs-tab">
-// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
-// Diffs
-// </a>
-// </li>
-// </ul>
-//
-// <div class="tab-content">
-// <div class="notes tab-pane active" id="notes">
-// Notes Content
-// </div>
-// <div class="commits tab-pane" id="commits">
-// Commits Content
-// </div>
-// <div class="diffs tab-pane" id="diffs">
-// Diffs Content
-// </div>
-// </div>
-//
-// <div class="mr-loading-status">
-// <div class="loading">
-// Loading Animation
-// </div>
-// </div>
-//
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.MergeRequestTabs = (function() {
- MergeRequestTabs.prototype.diffsLoaded = false;
-
- MergeRequestTabs.prototype.buildsLoaded = false;
-
- MergeRequestTabs.prototype.pipelinesLoaded = false;
-
- MergeRequestTabs.prototype.commitsLoaded = false;
-
- MergeRequestTabs.prototype.fixedLayoutPref = null;
-
- function MergeRequestTabs(opts) {
- this.opts = opts != null ? opts : {};
- this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
-
- this.buildsLoaded = this.opts.buildsLoaded || false;
-
- this.setCurrentAction = bind(this.setCurrentAction, this);
- this.tabShown = bind(this.tabShown, this);
- this.showTab = bind(this.showTab, this);
- // Store the `location` object, allowing for easier stubbing in tests
- this._location = location;
- this.bindEvents();
- this.activateTab(this.opts.action);
- this.initAffix();
- }
-
- MergeRequestTabs.prototype.bindEvents = function() {
- $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
- $(document).on('click', '.js-show-tab', this.showTab);
- };
-
- MergeRequestTabs.prototype.unbindEvents = function() {
- $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
- $(document).off('click', '.js-show-tab', this.showTab);
- };
-
- MergeRequestTabs.prototype.showTab = function(event) {
- event.preventDefault();
- return this.activateTab($(event.target).data('action'));
- };
-
- MergeRequestTabs.prototype.tabShown = function(event) {
- var $target, action, navBarHeight;
- $target = $(event.target);
- action = $target.data('action');
- if (action === 'commits') {
- this.loadCommits($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- } else if (this.isDiffAction(action)) {
- this.loadDiff($target.attr('href'));
- if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
- this.shrinkView();
- }
- if (this.diffViewType() === 'parallel') {
- this.expandViewContainer();
- }
- navBarHeight = $('.navbar-gitlab').outerHeight();
- $.scrollTo(".merge-request-details .merge-request-tabs", {
- offset: -navBarHeight
- });
- } else if (action === 'builds') {
- this.loadBuilds($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- } else if (action === 'pipelines') {
- this.loadPipelines($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- } else {
- this.expandView();
- this.resetViewContainer();
- }
- if (this.opts.setUrl) {
- this.setCurrentAction(action);
- }
- };
-
- MergeRequestTabs.prototype.scrollToElement = function(container) {
- var $el, navBarHeight;
- if (window.location.hash) {
- navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
- $el = $(container + " " + window.location.hash + ":not(.match)");
- if ($el.length) {
- return $.scrollTo(container + " " + window.location.hash + ":not(.match)", {
- offset: -navBarHeight
- });
- }
- }
- };
-
- // Activate a tab based on the current action
- MergeRequestTabs.prototype.activateTab = function(action) {
- if (action === 'show') {
- action = 'notes';
- }
- $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
- };
-
- // Replaces the current Merge Request-specific action in the URL with a new one
- //
- // If the action is "notes", the URL is reset to the standard
- // `MergeRequests#show` route.
- //
- // Examples:
- //
- // location.pathname # => "/namespace/project/merge_requests/1"
- // setCurrentAction('diffs')
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- //
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- // setCurrentAction('notes')
- // location.pathname # => "/namespace/project/merge_requests/1"
- //
- // location.pathname # => "/namespace/project/merge_requests/1/diffs"
- // setCurrentAction('commits')
- // location.pathname # => "/namespace/project/merge_requests/1/commits"
- //
- // Returns the new URL String
- MergeRequestTabs.prototype.setCurrentAction = function(action) {
- var new_state;
- // Normalize action, just to be safe
- if (action === 'show') {
- action = 'notes';
- }
- this.currentAction = action;
- // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
- new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
-
- // Append the new action if we're on a tab other than 'notes'
- if (action !== 'notes') {
- new_state += "/" + action;
- }
- // Ensure parameters and hash come along for the ride
- new_state += this._location.search + this._location.hash;
- history.replaceState({
- turbolinks: true,
- url: new_state
- // Replace the current history state with the new one without breaking
- // Turbolinks' history.
- //
- // See https://github.com/rails/turbolinks/issues/363
- }, document.title, new_state);
- return new_state;
- };
-
- MergeRequestTabs.prototype.loadCommits = function(source) {
- if (this.commitsLoaded) {
- return;
- }
- return this._get({
- url: source + ".json",
- success: (function(_this) {
- return function(data) {
- document.querySelector("div#commits").innerHTML = data.html;
- gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
- _this.commitsLoaded = true;
- return _this.scrollToElement("#commits");
- };
- })(this)
- });
- };
-
- MergeRequestTabs.prototype.loadDiff = function(source) {
- if (this.diffsLoaded) {
- return;
- }
-
- // We extract pathname for the current Changes tab anchor href
- // some pages like MergeRequestsController#new has query parameters on that anchor
- var url = gl.utils.parseUrl(source);
-
- return this._get({
- url: (url.pathname + ".json") + this._location.search,
- success: (function(_this) {
- return function(data) {
- $('#diffs').html(data.html);
-
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
- }
-
- gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
- $('#diffs .js-syntax-highlight').syntaxHighlight();
- $('#diffs .diff-file').singleFileDiff();
- if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
- _this.expandViewContainer();
- }
- _this.diffsLoaded = true;
- var anchoredDiff = gl.utils.getLocationHash();
- if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() {
- _this.scrollToElement("#diffs");
- _this.highlighSelectedLine();
- });
- _this.filesCommentButton = $('.files .diff-file').filesCommentButton();
- return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) {
- e.preventDefault();
- window.location.hash = $(e.currentTarget).attr('href');
- _this.highlighSelectedLine();
- return _this.scrollToElement("#diffs");
- });
- };
- })(this)
- });
- };
-
- MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) {
- var diffTitle = $('#file-path-' + anchoredDiff);
- var diffFile = diffTitle.closest('.diff-file');
- var nothingHereBlock = $('.nothing-here-block:visible', diffFile);
- if (nothingHereBlock.length) {
- diffFile.singleFileDiff(true, cb);
- } else {
- cb();
- }
- };
-
- MergeRequestTabs.prototype.highlighSelectedLine = function() {
- var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight;
- $('.hll').removeClass('hll');
- locationHash = window.location.hash;
- if (locationHash !== '') {
- dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]';
- $diffLine = $(locationHash + ":not(.match)", $('#diffs'));
- if (!$diffLine.is('tr')) {
- $diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString);
- } else {
- $diffLine = $diffLine.find('td');
- }
- if ($diffLine.length) {
- $diffLine.addClass('hll');
- diffLineTop = $diffLine.offset().top;
- return navBarHeight = $('.navbar-gitlab').outerHeight();
- }
- }
- };
-
- MergeRequestTabs.prototype.loadBuilds = function(source) {
- if (this.buildsLoaded) {
- return;
- }
- return this._get({
- url: source + ".json",
- success: (function(_this) {
- return function(data) {
- document.querySelector("div#builds").innerHTML = data.html;
- gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
- _this.buildsLoaded = true;
- if (!this.pipelines) this.pipelines = new gl.Pipelines();
- return _this.scrollToElement("#builds");
- };
- })(this)
- });
- };
-
- MergeRequestTabs.prototype.loadPipelines = function(source) {
- if (this.pipelinesLoaded) {
- return;
- }
- return this._get({
- url: source + ".json",
- success: function(data) {
- $('#pipelines').html(data.html);
- gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
- this.pipelinesLoaded = true;
- return this.scrollToElement("#pipelines");
- }.bind(this)
- });
- };
-
- // Show or hide the loading spinner
- //
- // status - Boolean, true to show, false to hide
- MergeRequestTabs.prototype.toggleLoading = function(status) {
- return $('.mr-loading-status .loading').toggle(status);
- };
-
- MergeRequestTabs.prototype._get = function(options) {
- var defaults;
- defaults = {
- beforeSend: (function(_this) {
- return function() {
- return _this.toggleLoading(true);
- };
- })(this),
- complete: (function(_this) {
- return function() {
- return _this.toggleLoading(false);
- };
- })(this),
- dataType: 'json',
- type: 'GET'
- };
- options = $.extend({}, defaults, options);
- return $.ajax(options);
- };
-
- MergeRequestTabs.prototype.diffViewType = function() {
- return $('.inline-parallel-buttons a.active').data('view-type');
- };
-
- MergeRequestTabs.prototype.isDiffAction = function(action) {
- return action === 'diffs' || action === 'new/diffs'
- };
-
- MergeRequestTabs.prototype.expandViewContainer = function() {
- var $wrapper = $('.content-wrapper .container-fluid');
- if (this.fixedLayoutPref === null) {
- this.fixedLayoutPref = $wrapper.hasClass('container-limited');
- }
- $wrapper.removeClass('container-limited');
- };
-
- MergeRequestTabs.prototype.resetViewContainer = function() {
- if (this.fixedLayoutPref !== null) {
- $('.content-wrapper .container-fluid')
- .toggleClass('container-limited', this.fixedLayoutPref);
- }
- };
-
- MergeRequestTabs.prototype.shrinkView = function() {
- var $gutterIcon;
- $gutterIcon = $('.js-sidebar-toggle i:visible');
- return setTimeout(function() {
- if ($gutterIcon.is('.fa-angle-double-right')) {
- return $gutterIcon.closest('a').trigger('click', [true]);
- }
- // Wait until listeners are set
- // Only when sidebar is expanded
- }, 0);
- };
-
- MergeRequestTabs.prototype.expandView = function() {
- var $gutterIcon;
- if (Cookies.get('collapsed_gutter') === 'true') {
- return;
- }
- $gutterIcon = $('.js-sidebar-toggle i:visible');
- return setTimeout(function() {
- if ($gutterIcon.is('.fa-angle-double-left')) {
- return $gutterIcon.closest('a').trigger('click', [true]);
- }
- }, 0);
- // Expand the issuable sidebar unless the user explicitly collapsed it
- // Wait until listeners are set
- // Only when sidebar is collapsed
- };
-
- MergeRequestTabs.prototype.initAffix = function () {
- var $tabs = $('.js-tabs-affix');
-
- // Screen space on small screens is usually very sparse
- // So we dont affix the tabs on these
- if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
-
- var $diffTabs = $('#diff-notes-app'),
- $fixedNav = $('.navbar-fixed-top'),
- $layoutNav = $('.layout-nav');
-
- $tabs.off('affix.bs.affix affix-top.bs.affix')
- .affix({
- offset: {
- top: function () {
- var tabsTop = $diffTabs.offset().top - $tabs.height();
- tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height());
-
- return tabsTop;
- }
- }
- }).on('affix.bs.affix', function () {
- $diffTabs.css({
- marginTop: $tabs.height()
- });
- }).on('affix-top.bs.affix', function () {
- $diffTabs.css({
- marginTop: ''
- });
- });
-
- // Fix bug when reloading the page already scrolling
- if ($tabs.hasClass('affix')) {
- $tabs.trigger('affix.bs.affix');
- }
- };
-
- return MergeRequestTabs;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6
new file mode 100644
index 00000000000..4c8c28af755
--- /dev/null
+++ b/app/assets/javascripts/merge_request_tabs.js.es6
@@ -0,0 +1,365 @@
+/* eslint-disable no-new, class-methods-use-this */
+/* global Breakpoints */
+/* global Cookies */
+/* global DiffNotesApp */
+/* global Flash */
+
+/*= require js.cookie */
+/*= require breakpoints */
+
+/* eslint-disable max-len */
+// MergeRequestTabs
+//
+// Handles persisting and restoring the current tab selection and lazily-loading
+// content on the MergeRequests#show page.
+//
+// ### Example Markup
+//
+// <ul class="nav-links merge-request-tabs">
+// <li class="notes-tab active">
+// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
+// Discussion
+// </a>
+// </li>
+// <li class="commits-tab">
+// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
+// Commits
+// </a>
+// </li>
+// <li class="diffs-tab">
+// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
+// Diffs
+// </a>
+// </li>
+// </ul>
+//
+// <div class="tab-content">
+// <div class="notes tab-pane active" id="notes">
+// Notes Content
+// </div>
+// <div class="commits tab-pane" id="commits">
+// Commits Content
+// </div>
+// <div class="diffs tab-pane" id="diffs">
+// Diffs Content
+// </div>
+// </div>
+//
+// <div class="mr-loading-status">
+// <div class="loading">
+// Loading Animation
+// </div>
+// </div>
+//
+/* eslint-enable max-len */
+
+(() => {
+ // Store the `location` object, allowing for easier stubbing in tests
+ let location = window.location;
+
+ class MergeRequestTabs {
+
+ constructor({ action, setUrl, stubLocation } = {}) {
+ this.diffsLoaded = false;
+ this.pipelinesLoaded = false;
+ this.commitsLoaded = false;
+ this.fixedLayoutPref = null;
+
+ this.setUrl = setUrl !== undefined ? setUrl : true;
+ this.setCurrentAction = this.setCurrentAction.bind(this);
+ this.tabShown = this.tabShown.bind(this);
+ this.showTab = this.showTab.bind(this);
+
+ if (stubLocation) {
+ location = stubLocation;
+ }
+
+ this.bindEvents();
+ this.activateTab(action);
+ this.initAffix();
+ }
+
+ bindEvents() {
+ $(document)
+ .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
+ .on('click', '.js-show-tab', this.showTab);
+ }
+
+ unbindEvents() {
+ $(document)
+ .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
+ .off('click', '.js-show-tab', this.showTab);
+ }
+
+ showTab(e) {
+ e.preventDefault();
+ this.activateTab($(e.target).data('action'));
+ }
+
+ tabShown(e) {
+ const $target = $(e.target);
+ const action = $target.data('action');
+
+ if (action === 'commits') {
+ this.loadCommits($target.attr('href'));
+ this.expandView();
+ this.resetViewContainer();
+ } else if (this.isDiffAction(action)) {
+ this.loadDiff($target.attr('href'));
+ if (Breakpoints.get().getBreakpointSize() !== 'lg') {
+ this.shrinkView();
+ }
+ if (this.diffViewType() === 'parallel') {
+ this.expandViewContainer();
+ }
+ const navBarHeight = $('.navbar-gitlab').outerHeight();
+ $.scrollTo('.merge-request-details .merge-request-tabs', {
+ offset: -navBarHeight,
+ });
+ } else if (action === 'pipelines') {
+ this.loadPipelines($target.attr('href'));
+ this.expandView();
+ this.resetViewContainer();
+ } else {
+ this.expandView();
+ this.resetViewContainer();
+ }
+ if (this.setUrl) {
+ this.setCurrentAction(action);
+ }
+ }
+
+ scrollToElement(container) {
+ if (location.hash) {
+ const offset = 0 - (
+ $('.navbar-gitlab').outerHeight() +
+ $('.layout-nav').outerHeight() +
+ $('.js-tabs-affix').outerHeight()
+ );
+ const $el = $(`${container} ${location.hash}:not(.match)`);
+ if ($el.length) {
+ $.scrollTo($el[0], { offset });
+ }
+ }
+ }
+
+ // Activate a tab based on the current action
+ activateTab(action) {
+ const activate = action === 'show' ? 'notes' : action;
+ // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
+ $(`.merge-request-tabs a[data-action='${activate}']`).tab('show');
+ }
+
+ // Replaces the current Merge Request-specific action in the URL with a new one
+ //
+ // If the action is "notes", the URL is reset to the standard
+ // `MergeRequests#show` route.
+ //
+ // Examples:
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ // setCurrentAction('diffs')
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('notes')
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('commits')
+ // location.pathname # => "/namespace/project/merge_requests/1/commits"
+ //
+ // Returns the new URL String
+ setCurrentAction(action) {
+ this.currentAction = action === 'show' ? 'notes' : action;
+
+ // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs'
+ let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
+
+ // Append the new action if we're on a tab other than 'notes'
+ if (this.currentAction !== 'notes') {
+ newState += `/${this.currentAction}`;
+ }
+
+ // Ensure parameters and hash come along for the ride
+ newState += location.search + location.hash;
+
+ // Replace the current history state with the new one without breaking
+ // Turbolinks' history.
+ //
+ // See https://github.com/rails/turbolinks/issues/363
+ window.history.replaceState({
+ turbolinks: true,
+ url: newState,
+ }, document.title, newState);
+
+ return newState;
+ }
+
+ loadCommits(source) {
+ if (this.commitsLoaded) {
+ return;
+ }
+ this.ajaxGet({
+ url: `${source}.json`,
+ success: (data) => {
+ document.querySelector('div#commits').innerHTML = data.html;
+ gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
+ this.commitsLoaded = true;
+ this.scrollToElement('#commits');
+ },
+ });
+ }
+
+ loadDiff(source) {
+ if (this.diffsLoaded) {
+ return;
+ }
+
+ // We extract pathname for the current Changes tab anchor href
+ // some pages like MergeRequestsController#new has query parameters on that anchor
+ const urlPathname = gl.utils.parseUrlPathname(source);
+
+ this.ajaxGet({
+ url: `${urlPathname}.json${location.search}`,
+ success: (data) => {
+ $('#diffs').html(data.html);
+
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
+ }
+
+ gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
+ $('#diffs .js-syntax-highlight').syntaxHighlight();
+
+ if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
+ this.expandViewContainer();
+ }
+ this.diffsLoaded = true;
+
+ new gl.Diff();
+ this.scrollToElement('#diffs');
+ },
+ });
+ }
+
+ loadPipelines(source) {
+ if (this.pipelinesLoaded) {
+ return;
+ }
+ this.ajaxGet({
+ url: `${source}.json`,
+ success: (data) => {
+ $('#pipelines').html(data.html);
+ gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
+ this.pipelinesLoaded = true;
+ this.scrollToElement('#pipelines');
+
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-table',
+ });
+ },
+ });
+ }
+
+ // Show or hide the loading spinner
+ //
+ // status - Boolean, true to show, false to hide
+ toggleLoading(status) {
+ $('.mr-loading-status .loading').toggle(status);
+ }
+
+ ajaxGet(options) {
+ const defaults = {
+ beforeSend: () => this.toggleLoading(true),
+ error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
+ complete: () => this.toggleLoading(false),
+ dataType: 'json',
+ type: 'GET',
+ };
+ $.ajax($.extend({}, defaults, options));
+ }
+
+ diffViewType() {
+ return $('.inline-parallel-buttons a.active').data('view-type');
+ }
+
+ isDiffAction(action) {
+ return action === 'diffs' || action === 'new/diffs';
+ }
+
+ expandViewContainer() {
+ const $wrapper = $('.content-wrapper .container-fluid');
+ if (this.fixedLayoutPref === null) {
+ this.fixedLayoutPref = $wrapper.hasClass('container-limited');
+ }
+ $wrapper.removeClass('container-limited');
+ }
+
+ resetViewContainer() {
+ if (this.fixedLayoutPref !== null) {
+ $('.content-wrapper .container-fluid')
+ .toggleClass('container-limited', this.fixedLayoutPref);
+ }
+ }
+
+ shrinkView() {
+ const $gutterIcon = $('.js-sidebar-toggle i:visible');
+
+ // Wait until listeners are set
+ setTimeout(() => {
+ // Only when sidebar is expanded
+ if ($gutterIcon.is('.fa-angle-double-right')) {
+ $gutterIcon.closest('a').trigger('click', [true]);
+ }
+ }, 0);
+ }
+
+ // Expand the issuable sidebar unless the user explicitly collapsed it
+ expandView() {
+ if (Cookies.get('collapsed_gutter') === 'true') {
+ return;
+ }
+ const $gutterIcon = $('.js-sidebar-toggle i:visible');
+
+ // Wait until listeners are set
+ setTimeout(() => {
+ // Only when sidebar is collapsed
+ if ($gutterIcon.is('.fa-angle-double-left')) {
+ $gutterIcon.closest('a').trigger('click', [true]);
+ }
+ }, 0);
+ }
+
+ initAffix() {
+ const $tabs = $('.js-tabs-affix');
+
+ // Screen space on small screens is usually very sparse
+ // So we dont affix the tabs on these
+ if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
+
+ const $diffTabs = $('#diff-notes-app');
+ const $fixedNav = $('.navbar-fixed-top');
+ const $layoutNav = $('.layout-nav');
+
+ $tabs.off('affix.bs.affix affix-top.bs.affix')
+ .affix({
+ offset: {
+ top: () => (
+ $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height()
+ ),
+ },
+ })
+ .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
+ .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' }));
+
+ // Fix bug when reloading the page already scrolling
+ if ($tabs.hasClass('affix')) {
+ $tabs.trigger('affix.bs.affix');
+ }
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.MergeRequestTabs = MergeRequestTabs;
+})();
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 56c87af3226..7cc319e2f4e 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -1,6 +1,11 @@
-/* eslint-disable */
- ((global) => {
- var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, camelcase, default-case, wrap-iife */
+/* global notify */
+/* global notifyPermissions */
+/* global merge_request_widget */
+/* global Turbolinks */
+
+((global) => {
+ var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
<div class="ci_widget ci-success">
@@ -27,7 +32,7 @@
</div>
</div>`;
- global.MergeRequestWidget = (function() {
+ global.MergeRequestWidget = (function() {
function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior
//
@@ -40,19 +45,26 @@
$('#modal_merge_info').modal({
show: false
});
- this.firstCICheck = true;
- this.readyForCICheck = false;
- this.readyForCIEnvironmentCheck = false;
- this.cancel = false;
- clearInterval(this.fetchBuildStatusInterval);
- clearInterval(this.fetchBuildEnvironmentStatusInterval);
this.clearEventListeners();
this.addEventListeners();
this.getCIStatus(false);
- this.getCIEnvironmentsStatus();
this.retrieveSuccessIcon();
- this.pollCIStatus();
- this.pollCIEnvironmentsStatus();
+
+ this.ciStatusInterval = new global.SmartInterval({
+ callback: this.getCIStatus.bind(this, true),
+ startingInterval: 10000,
+ maxInterval: 30000,
+ hiddenInterval: 120000,
+ incrementByFactorOf: 5000,
+ });
+ this.ciEnvironmentStatusInterval = new global.SmartInterval({
+ callback: this.getCIEnvironmentsStatus.bind(this),
+ startingInterval: 30000,
+ maxInterval: 120000,
+ hiddenInterval: 240000,
+ incrementByFactorOf: 15000,
+ immediateExecution: true,
+ });
notifyPermissions();
}
@@ -60,21 +72,14 @@
return $(document).off('page:change.merge_request');
};
- MergeRequestWidget.prototype.cancelPolling = function() {
- return this.cancel = true;
- };
-
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
- allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
- return $(document).on('page:change.merge_request', (function(_this) {
+ allowedPages = ['show', 'commits', 'pipelines', 'changes'];
+ $(document).on('page:change.merge_request', (function(_this) {
return function() {
var page;
page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) {
- clearInterval(_this.fetchBuildStatusInterval);
- clearInterval(_this.fetchBuildEnvironmentStatusInterval);
- _this.cancelPolling();
return _this.clearEventListeners();
}
};
@@ -82,10 +87,10 @@
};
MergeRequestWidget.prototype.retrieveSuccessIcon = function() {
- const $ciSuccessIcon = $('.js-success-icon');
- this.$ciSuccessIcon = $ciSuccessIcon.html();
- $ciSuccessIcon.remove();
- }
+ const $ciSuccessIcon = $('.js-success-icon');
+ this.$ciSuccessIcon = $ciSuccessIcon.html();
+ $ciSuccessIcon.remove();
+ };
MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
if (deleteSourceBranch == null) {
@@ -101,7 +106,7 @@
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
- return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
+ return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
} else {
callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch);
@@ -114,9 +119,16 @@
});
};
+ MergeRequestWidget.prototype.cancelPolling = function () {
+ this.ciStatusInterval.cancel();
+ this.ciEnvironmentStatusInterval.cancel();
+ };
+
MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, function(data) {
- return $('.mr-state-widget').replaceWith(data);
+ var $html = $(data);
+ $('.mr-widget-body').replaceWith($html.find('.mr-widget-body'));
+ $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer'));
});
};
@@ -131,18 +143,6 @@
}
};
- MergeRequestWidget.prototype.pollCIStatus = function() {
- return this.fetchBuildStatusInterval = setInterval(((function(_this) {
- return function() {
- if (!_this.readyForCICheck) {
- return;
- }
- _this.getCIStatus(true);
- return _this.readyForCICheck = false;
- };
- })(this)), 10000);
- };
-
MergeRequestWidget.prototype.getCIStatus = function(showNotification) {
var _this;
_this = this;
@@ -150,23 +150,17 @@
return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) {
var message, status, title;
- if (_this.cancel) {
- return;
- }
- _this.readyForCICheck = true;
if (data.status === '') {
return;
}
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
- if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) {
+ if (data.status !== _this.opts.ci_status && (data.status != null)) {
_this.opts.ci_status = data.status;
_this.showCIStatus(data.status);
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
- // The first check should only update the UI, a notification
- // should only be displayed on status changes
- if (showNotification && !_this.firstCICheck) {
+ if (showNotification) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
title = _this.opts.ci_title.preparing;
@@ -181,54 +175,42 @@
message = message.replace('{{title}}', data.title);
notify(title, message, _this.opts.gitlab_icon, function() {
this.close();
- return Turbolinks.visit(_this.opts.builds_path);
});
}
- return _this.firstCICheck = false;
}
};
})(this));
};
- MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
- this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
- if (!this.readyForCIEnvironmentCheck) return;
- this.getCIEnvironmentsStatus();
- this.readyForCIEnvironmentCheck = false;
- }, 300000);
- };
-
MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
$.getJSON(this.opts.ci_environments_status_url, (environments) => {
- if (this.cancel) return;
- this.readyForCIEnvironmentCheck = true;
if (environments && environments.length) this.renderEnvironments(environments);
});
};
MergeRequestWidget.prototype.renderEnvironments = function(environments) {
- for (let i = 0; i < environments.length; i++) {
+ for (let i = 0; i < environments.length; i += 1) {
const environment = environments[i];
- if ($(`.mr-state-widget #${ environment.id }`).length) return;
+ if ($(`.mr-state-widget #${environment.id}`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
-
+
if (!environment.stop_url) {
$('.js-stop-env-link', $template).remove();
}
-
+
if (environment.deployed_at && environment.deployed_at_formatted) {
- environment.deployed_at = gl.utils.getTimeago(environment.deployed_at) + '.';
+ environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
} else {
$('.js-environment-timeago', $template).remove();
environment.name += '.';
}
environment.ci_success_icon = this.$ciSuccessIcon;
const templateString = _.unescape($template[0].outerHTML);
- const template = _.template(templateString)(environment)
+ const template = _.template(templateString)(environment);
this.$widgetBody.before(template);
}
- };
+ };
MergeRequestWidget.prototype.showCIStatus = function(state) {
var allowed_states;
@@ -245,7 +227,7 @@
case "not_found":
return this.setMergeButtonClass('btn-danger');
case "running":
- return this.setMergeButtonClass('btn-warning');
+ return this.setMergeButtonClass('btn-info');
case "success":
case "success_with_warnings":
return this.setMergeButtonClass('btn-create');
@@ -263,11 +245,9 @@
};
MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) {
- return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-warning btn-create').addClass(css_class);
+ return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class);
};
return MergeRequestWidget;
-
})();
-
- })(window.gl || (window.gl = {}));
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
new file mode 100644
index 00000000000..5969d2ba56b
--- /dev/null
+++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
@@ -0,0 +1,53 @@
+/* global merge_request_widget */
+
+(() => {
+ $(() => {
+ /* TODO: This needs a better home, or should be refactored. It was previously contained
+ * in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml,
+ * but Vue chokes on script tags and prevents their execution. So it was moved here
+ * temporarily.
+ * */
+
+ $(document)
+ .off('ajax:send', '.accept-mr-form')
+ .on('ajax:send', '.accept-mr-form', () => {
+ $('.accept-mr-form :input').disable();
+ });
+
+ $(document)
+ .off('click', '.accept_merge_request')
+ .on('click', '.accept_merge_request', () => {
+ $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
+ });
+
+ $(document)
+ .off('click', '.merge_when_build_succeeds')
+ .on('click', '.merge_when_build_succeeds', () => {
+ $('#merge_when_build_succeeds').val('1');
+ });
+
+ $(document)
+ .off('click', '.js-merge-dropdown a')
+ .on('click', '.js-merge-dropdown a', (e) => {
+ e.preventDefault();
+ $(e.target).closest('form').submit();
+ });
+ if ($('.rebase-in-progress').length) {
+ merge_request_widget.rebaseInProgress();
+ } else if ($('.rebase-mr-form').length) {
+ $(document)
+ .off('ajax:send', '.rebase-mr-form')
+ .on('ajax:send', '.rebase-mr-form', () => {
+ $('.rebase-mr-form :input').disable();
+ });
+
+ $(document)
+ .off('click', '.js-rebase-button')
+ .on('click', '.js-rebase-button', () => {
+ $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
+ });
+ } else {
+ merge_request_widget.getMergeStatus();
+ }
+ });
+})();
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 7ad86d8c084..527cdc9b698 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.MergedButtons = (function() {
function MergedButtons() {
@@ -40,7 +41,5 @@
};
return MergedButtons;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 9299c96e8ea..7ce1259e015 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
+/* global Flash */
+
(function() {
this.Milestone = (function() {
Milestone.updateIssue = function(li, issue_url, data) {
@@ -191,7 +193,5 @@
};
return Milestone;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index d1cd38ad110..7ab39ffbd05 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
+/* global Vue */
+/* global Issuable */
+/* global ListMilestone */
+
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject) {
@@ -177,7 +181,5 @@
}
return MilestoneSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
new file mode 100644
index 00000000000..80549532ea9
--- /dev/null
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
@@ -0,0 +1,97 @@
+/* eslint-disable no-new */
+/* global Flash */
+
+/**
+ * In each pipelines table we have a mini pipeline graph for each pipeline.
+ *
+ * When we click in a pipeline stage, we need to make an API call to get the
+ * builds list to render in a dropdown.
+ *
+ * The container should be the table element.
+ *
+ * The stage icon clicked needs to have the following HTML structure:
+ * <div class="dropdown">
+ * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button>
+ * <div class="js-builds-dropdown-container dropdown-menu"></div>
+ * </div>
+ */
+(() => {
+ class MiniPipelineGraph {
+ constructor(opts = {}) {
+ this.container = opts.container || '';
+ this.dropdownListSelector = '.js-builds-dropdown-container';
+ this.getBuildsList = this.getBuildsList.bind(this);
+
+ this.bindEvents();
+ }
+
+ /**
+ * Adds the event listener when the dropdown is opened.
+ * All dropdown events are fired at the .dropdown-menu's parent element.
+ */
+ bindEvents() {
+ $(this.container).on('shown.bs.dropdown', this.getBuildsList);
+ }
+
+ /**
+ * For the clicked stage, renders the given data in the dropdown list.
+ *
+ * @param {HTMLElement} stageContainer
+ * @param {Object} data
+ */
+ renderBuildsList(stageContainer, data) {
+ const dropdownContainer = stageContainer.parentElement.querySelector(
+ `${this.dropdownListSelector} .js-builds-dropdown-list`,
+ );
+
+ dropdownContainer.innerHTML = data;
+ }
+
+ /**
+ * For the clicked stage, gets the list of builds.
+ *
+ * All dropdown events have a relatedTarget property,
+ * whose value is the toggling anchor element.
+ *
+ * @param {Object} e bootstrap dropdown event
+ * @return {Promise}
+ */
+ getBuildsList(e) {
+ const button = e.relatedTarget;
+ const endpoint = button.dataset.stageEndpoint;
+
+ return $.ajax({
+ dataType: 'json',
+ type: 'GET',
+ url: endpoint,
+ beforeSend: () => {
+ this.renderBuildsList(button, '');
+ this.toggleLoading(button);
+ },
+ success: (data) => {
+ this.toggleLoading(button);
+ this.renderBuildsList(button, data.html);
+ },
+ error: () => {
+ this.toggleLoading(button);
+ new Flash('An error occurred while fetching the builds.', 'alert');
+ },
+ });
+ }
+
+ /**
+ * Toggles the visibility of the loading icon.
+ *
+ * @param {HTMLElement} stageContainer
+ * @return {type}
+ */
+ toggleLoading(stageContainer) {
+ stageContainer.parentElement.querySelector(
+ `${this.dropdownListSelector} .js-builds-dropdown-loading`,
+ ).classList.toggle('hidden');
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.MiniPipelineGraph = MiniPipelineGraph;
+})();
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index d1168227b77..514556ade0b 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,8 +1,10 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */
+/* global Api */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
- this.NamespaceSelect = (function() {
+ window.NamespaceSelect = (function() {
function NamespaceSelect(opts) {
this.onSelectItem = bind(this.onSelectItem, this);
var fieldName, showAny;
@@ -61,10 +63,9 @@
};
return NamespaceSelect;
-
})();
- this.NamespaceSelects = (function() {
+ window.NamespaceSelects = (function() {
function NamespaceSelects(opts) {
var ref;
if (opts == null) {
@@ -74,14 +75,12 @@
this.$dropdowns.each(function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
- return new NamespaceSelect({
+ return new window.NamespaceSelect({
dropdown: $dropdown
});
});
}
return NamespaceSelects;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 74dbeb94741..a7ccd03b60c 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,6 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
+/* global Raphael */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.BranchGraph = (function() {
function BranchGraph(element1, options1) {
@@ -51,7 +53,7 @@
this.top = this.r.set();
this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
ref = this.commits;
- for (j = 0, len = ref.length; j < len; j++) {
+ for (j = 0, len = ref.length; j < len; j += 1) {
c = ref[j];
if (c.id in this.parents) {
c.isParent = true;
@@ -66,7 +68,7 @@
var c, j, len, p, ref, results;
ref = this.commits;
results = [];
- for (j = 0, len = ref.length; j < len; j++) {
+ for (j = 0, len = ref.length; j < len; j += 1) {
c = ref[j];
this.mtime = Math.max(this.mtime, c.time);
this.mspace = Math.max(this.mspace, c.space);
@@ -74,7 +76,7 @@
var l, len1, ref1, results1;
ref1 = c.parents;
results1 = [];
- for (l = 0, len1 = ref1.length; l < len1; l++) {
+ for (l = 0, len1 = ref1.length; l < len1; l += 1) {
p = ref1[l];
this.parents[p[0]] = true;
results1.push(this.mspace = Math.max(this.mspace, p[1]));
@@ -94,7 +96,7 @@
// Skipping a few colors in the spectrum to get more contrast between colors
Raphael.getColor();
Raphael.getColor();
- results.push(k++);
+ results.push(k += 1);
}
return results;
};
@@ -111,7 +113,7 @@
fill: "#444"
});
ref = this.days;
- for (mm = j = 0, len = ref.length; j < len; mm = ++j) {
+ for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
// Dates
@@ -284,7 +286,7 @@
r = this.r;
ref = commit.parents;
results = [];
- for (i = j = 0, len = ref.length; j < len; i = ++j) {
+ for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
parent = ref[i];
parentCommit = this.preparedCommits[parent[0]];
parentY = this.offsetY + this.unitTime * parentCommit.time;
@@ -344,7 +346,6 @@
};
return BranchGraph;
-
})();
Raphael.prototype.commitTooltip = function(x, y, commit) {
@@ -354,7 +355,7 @@
icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20);
nameText = this.text(x + 25, y + 10, commit.author.name);
idText = this.text(x, y + 35, commit.id);
- messageText = this.text(x, y + 50, commit.message);
+ messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n "));
textSet = this.set(icon, nameText, idText, messageText).attr({
"text-anchor": "start",
font: "12px Monaco, monospace"
@@ -366,6 +367,7 @@
idText.attr({
fill: "#AAA"
});
+ messageText.node.style["white-space"] = "pre";
this.textWrap(messageText, boxWidth - 50);
rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
fill: "#FFF",
@@ -396,23 +398,27 @@
words = content.split(" ");
x = 0;
s = [];
- for (j = 0, len = words.length; j < len; j++) {
+ for (j = 0, len = words.length; j < len; j += 1) {
word = words[j];
if (x + (word.length * letterWidth) > width) {
s.push("\n");
x = 0;
}
- x += word.length * letterWidth;
- s.push(word + " ");
+ if (word === "\n") {
+ s.push("\n");
+ x = 0;
+ } else {
+ s.push(word + " ");
+ x += word.length * letterWidth;
+ }
}
t.attr({
- text: s.join("")
+ text: s.join("").trim()
});
b = t.getBBox();
- h = Math.abs(b.y2) - Math.abs(b.y) + 1;
+ h = Math.abs(b.y2) + 1;
return t.attr({
- y: b.y + h
+ y: h
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 8898e7ace43..37bf6436fd1 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
+/* global BranchGraph */
+
(function() {
this.Network = (function() {
function Network(opts) {
@@ -14,7 +16,5 @@
}
return Network;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index a192273a180..2e6eb83cec7 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
+/* global Network */
+/* global ShortcutsNetwork */
+
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
@@ -20,5 +23,4 @@
});
return new ShortcutsNetwork(network_graph.branch_graph);
});
-
}).call(this);
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 0e643b0ff14..7f763c13b50 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,7 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
- indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
this.NewBranchForm = (function() {
function NewBranchForm(form, availableRefs) {
@@ -99,7 +99,5 @@
};
return NewBranchForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index acb529023fa..41eea78a3e6 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,6 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.NewCommitForm = (function() {
function NewCommitForm(form) {
@@ -29,7 +29,5 @@
};
return NewCommitForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 4976eef2896..06a72efa21d 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,4 +1,9 @@
-/* eslint-disable */
+/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */
+/* global Flash */
+/* global GLForm */
+/* global Autosave */
+/* global ResolveService */
+/* global mrRefreshWidgetUrl */
/*= require autosave */
/*= require autosize */
@@ -9,10 +14,10 @@
/*= require task_list */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Notes = (function() {
- var isMetaKey;
+ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
Notes.interval = null;
@@ -33,6 +38,7 @@
this.resetMainTargetForm = bind(this.resetMainTargetForm, this);
this.refresh = bind(this.refresh, this);
this.keydownNoteText = bind(this.keydownNoteText, this);
+ this.toggleCommitList = bind(this.toggleCommitList, this);
this.notes_url = notes_url;
this.note_ids = note_ids;
this.last_fetched_at = last_fetched_at;
@@ -46,6 +52,13 @@
this.setPollingInterval();
this.setupMainTargetNoteForm();
this.initTaskList();
+ this.collapseLongCommitList();
+
+ // We are in the Merge Requests page so we need another edit form for Changes tab
+ if (gl.utils.getPagePath(1) === 'merge_requests') {
+ $('.note-edit-form').clone()
+ .addClass('mr-note-edit-form').insertAfter('.note-edit-form');
+ }
}
Notes.prototype.addBinding = function() {
@@ -57,7 +70,7 @@
// change note in UI after update
$(document).on("ajax:success", "form.edit-note", this.updateNote);
// Edit note link
- $(document).on("click", ".js-note-edit", this.showEditForm);
+ $(document).on("click", ".js-note-edit", this.showEditForm.bind(this));
$(document).on("click", ".note-edit-cancel", this.cancelEdit);
// Reopen and close actions for Issue/MR combined with note form submit
$(document).on("click", ".js-comment-button", this.updateCloseButton);
@@ -81,10 +94,13 @@
$(document).on("click", ".js-add-diff-note-button", this.addDiffNote);
// hide diff note form
$(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
+ // toggle commit list
+ $(document).on("click", '.system-note-commit-list-toggler', this.toggleCommitList);
// fetch notes when tab becomes visible
$(document).on("visibilitychange", this.visibilityChange);
// when issue status changes, we need to refresh data
$(document).on("issuable:change", this.refresh);
+
// when a key is clicked on the notes
return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
};
@@ -108,15 +124,17 @@
$(document).off("click", ".js-note-discard");
$(document).off("keydown", ".js-note-text");
$(document).off('click', '.js-comment-resolve-button');
+ $(document).off("click", '.system-note-commit-list-toggler');
$('.note .js-task-list-container').taskList('disable');
return $(document).off('tasklist:changed', '.note .js-task-list-container');
};
Notes.prototype.keydownNoteText = function(e) {
var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText;
- if (isMetaKey(e)) {
+ if (gl.utils.isMetaKey(e)) {
return;
}
+
$textarea = $(e.target);
// Edit previous note when UP arrow is hit
switch (e.which) {
@@ -156,10 +174,6 @@
}
};
- isMetaKey = function(e) {
- return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
- };
-
Notes.prototype.initRefresh = function() {
clearInterval(Notes.interval);
return Notes.interval = setInterval((function(_this) {
@@ -231,6 +245,16 @@
};
+ Notes.prototype.handleCreateChanges = function(note) {
+ if (typeof note === 'undefined') {
+ return;
+ }
+
+ if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) {
+ $.get(mrRefreshWidgetUrl);
+ }
+ };
+
/*
Render note in main comments area.
@@ -263,6 +287,7 @@
$notesList.append(note.html).syntaxHighlight();
// Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
+ this.collapseLongCommitList();
this.initTaskList();
this.refresh();
return this.updateNotesCount(1);
@@ -301,7 +326,7 @@
}
row = form.closest("tr");
note_html = $(note.html);
- note_html.syntaxHighlight();
+ note_html.renderGFM();
// is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
@@ -318,18 +343,18 @@
discussionContainer.append(note_html);
// Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
- $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight();
+ $('ul.main-notes-list').append(note.discussion_html).renderGFM();
}
} else {
// append new note to all matching discussions
discussionContainer.append(note_html);
}
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
- gl.utils.localTimeAgo($('.js-timeago', note_html), false);
+ gl.utils.localTimeAgo($('.js-timeago'), false);
return this.updateNotesCount(1);
};
@@ -415,6 +440,7 @@
*/
Notes.prototype.addNote = function(xhr, note, status) {
+ this.handleCreateChanges(note);
return this.renderNote(note);
};
@@ -433,9 +459,9 @@
var $form = $(xhr.target);
if ($form.attr('data-resolve-all') != null) {
- var projectPath = $form.data('project-path')
- discussionId = $form.data('discussion-id'),
- mergeRequestId = $form.data('noteable-iid');
+ var projectPath = $form.data('project-path');
+ var discussionId = $form.data('discussion-id');
+ var mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
@@ -458,17 +484,42 @@
var $html, $note_li;
// Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html);
+ this.revertNoteEditForm();
gl.utils.localTimeAgo($('.js-timeago', $html));
- $html.syntaxHighlight();
+ $html.renderGFM();
$html.find('.js-task-list-container').taskList('enable');
// Find the note's `li` element by ID and replace it with the updated HTML
$note_li = $('.note-row-' + note.id);
$note_li.replaceWith($html);
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
+ }
+ };
+
+
+ Notes.prototype.checkContentToAllowEditing = function($el) {
+ var initialContent = $el.find('.original-note-content').text().trim();
+ var currentContent = $el.find('.note-textarea').val();
+ var isAllowed = true;
+
+ if (currentContent === initialContent) {
+ this.removeNoteEditForm($el);
+ }
+ else {
+ var $buttons = $el.find('.note-form-actions');
+ var isWidgetVisible = gl.utils.isInViewport($el.get(0));
+
+ if (!isWidgetVisible) {
+ gl.utils.scrollToElement($el);
+ }
+
+ $el.find('.js-edit-warning').show();
+ isAllowed = false;
}
+
+ return isAllowed;
};
@@ -477,43 +528,27 @@
Replaces the note text with the note edit form
Adds a data attribute to the form with the original content of the note for cancellations
- */
-
+ */
Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) {
- var $noteText, done, form, note;
e.preventDefault();
- note = $(this).closest(".note");
- note.addClass("is-editting");
- form = note.find(".note-edit-form");
- form.addClass('current-note-edit-form');
- // Show the attachment delete link
- note.find(".js-note-attachment-delete").show();
- done = function($noteText) {
- var noteTextVal;
- // Neat little trick to put the cursor at the end
- noteTextVal = $noteText.val();
- // Store the original note text in a data attribute to retrieve if a user cancels edit.
- form.find('form.edit-note').data('original-note', noteTextVal);
- return $noteText.val('').val(noteTextVal);
- };
- new GLForm(form);
- if ((scrollTo != null) && (myLastNote != null)) {
- // scroll to the bottom
- // so the open of the last element doesn't make a jump
- $('html, body').scrollTop($(document).height());
- return $('html, body').animate({
- scrollTop: myLastNote.offset().top - 150
- }, 500, function() {
- var $noteText;
- $noteText = form.find(".js-note-text");
- $noteText.focus();
- return done($noteText);
- });
- } else {
- $noteText = form.find('.js-note-text');
- $noteText.focus();
- return done($noteText);
+
+ var $target = $(e.target);
+ var $editForm = $(this.getEditFormSelector($target));
+ var $note = $target.closest('.note');
+ var $currentlyEditing = $('.note.is-editting:visible');
+
+ if ($currentlyEditing.length) {
+ var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);
+
+ if (!isEditAllowed) {
+ return;
+ }
}
+
+ $note.find('.js-note-attachment-delete').show();
+ $editForm.addClass('current-note-edit-form');
+ $note.addClass('is-editting');
+ this.putEditFormInPlace($target);
};
@@ -524,19 +559,41 @@
*/
Notes.prototype.cancelEdit = function(e) {
- var note;
e.preventDefault();
- note = $(e.target).closest('.note');
+ var $target = $(e.target);
+ var note = $target.closest('.note');
+ note.find('.js-edit-warning').hide();
+ this.revertNoteEditForm($target);
return this.removeNoteEditForm(note);
};
+ Notes.prototype.revertNoteEditForm = function($target) {
+ $target = $target || $('.note.is-editting:visible');
+ var selector = this.getEditFormSelector($target);
+ var $editForm = $(selector);
+
+ $editForm.insertBefore('.notes-form');
+ $editForm.find('.js-comment-button').enable();
+ $editForm.find('.js-edit-warning').hide();
+ };
+
+ Notes.prototype.getEditFormSelector = function($el) {
+ var selector = '.note-edit-form:not(.mr-note-edit-form)';
+
+ if ($el.parents('#diffs').length) {
+ selector = '.note-edit-form.mr-note-edit-form';
+ }
+
+ return selector;
+ };
+
Notes.prototype.removeNoteEditForm = function(note) {
- var form;
- form = note.find(".current-note-edit-form");
- note.removeClass("is-editting");
- form.removeClass("current-note-edit-form");
+ var form = note.find('.current-note-edit-form');
+ note.removeClass('is-editting');
+ form.removeClass('current-note-edit-form');
+ form.find('.js-edit-warning').hide();
// Replace markdown textarea text with original note text.
- return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'));
+ return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
};
@@ -559,11 +616,9 @@
note = $(el);
notes = note.closest(".notes");
- if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) {
- ref = DiffNotesApp.$refs[noteId];
-
- if (ref) {
- ref.$destroy(true);
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ if (gl.diffNoteApps[noteId]) {
+ gl.diffNoteApps[noteId].$destroy();
}
}
@@ -643,11 +698,12 @@
form.find('.js-note-target-close').remove();
this.setupNoteForm(form);
- if (typeof DiffNotesApp !== 'undefined') {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
$commentBtn
.attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
- DiffNotesApp.$compile($commentBtn.get(0));
+
+ gl.diffNotesCompileComponents();
}
form.find(".js-note-text").focus();
@@ -668,7 +724,7 @@
*/
Notes.prototype.addDiffNote = function(e) {
- var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent;
+ var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent;
e.preventDefault();
$link = $(e.currentTarget);
row = $link.closest("tr");
@@ -830,24 +886,55 @@
Notes.prototype.initTaskList = function() {
this.enableTaskList();
- return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList);
+ return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList.bind(this));
};
Notes.prototype.enableTaskList = function() {
return $('.note .js-task-list-container').taskList('enable');
};
- Notes.prototype.updateTaskList = function() {
- return $('form', this).submit();
+ Notes.prototype.putEditFormInPlace = function($el) {
+ var $editForm = $(this.getEditFormSelector($el));
+ var $note = $el.closest('.note');
+
+ $editForm.insertAfter($note.find('.note-text'));
+
+ var $originalContentEl = $note.find('.original-note-content');
+ var originalContent = $originalContentEl.text().trim();
+ var postUrl = $originalContentEl.data('post-url');
+ var targetId = $originalContentEl.data('target-id');
+ var targetType = $originalContentEl.data('target-type');
+
+ new GLForm($editForm.find('form'));
+
+ $editForm.find('form')
+ .attr('action', postUrl)
+ .attr('data-remote', 'true');
+ $editForm.find('.js-form-target-id').val(targetId);
+ $editForm.find('.js-form-target-type').val(targetType);
+ $editForm.find('.js-note-text').focus().val(originalContent);
+ $editForm.find('.js-md-write-button').trigger('click');
+ $editForm.find('.referenced-users').hide();
+ };
+
+ Notes.prototype.updateTaskList = function(e) {
+ var $target = $(e.target);
+ var $list = $target.closest('.js-task-list-container');
+ var $editForm = $(this.getEditFormSelector($target));
+ var $note = $list.closest('.note');
+
+ this.putEditFormInPlace($list);
+ $editForm.find('#note_note').val($note.find('.original-task-list').val());
+ $('form', $list).submit();
};
Notes.prototype.updateNotesCount = function(updateCount) {
- return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount);
+ return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
};
- Notes.prototype.resolveDiscussion = function () {
- var $this = $(this),
- discussionId = $this.attr('data-discussion-id');
+ Notes.prototype.resolveDiscussion = function() {
+ var $this = $(this);
+ var discussionId = $this.attr('data-discussion-id');
$this
.closest('form')
@@ -856,8 +943,36 @@
.attr('data-project-path', $this.attr('data-project-path'));
};
- return Notes;
+ Notes.prototype.toggleCommitList = function(e) {
+ const $element = $(e.target);
+ const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
- })();
+ $closestSystemCommitList.toggleClass('hide-shade');
+ };
+
+ /**
+ Scans system notes with `ul` elements in system note body
+ then collapse long commit list pushed by user to make it less
+ intrusive.
+ */
+ Notes.prototype.collapseLongCommitList = function() {
+ const systemNotes = $('#notes-list').find('li.system-note').has('ul');
+ $.each(systemNotes, function(index, systemNote) {
+ const $systemNote = $(systemNote);
+ const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', '');
+
+ $systemNote.find('.note-header .system-note-message').html(headerMessage);
+
+ if ($systemNote.find('li').length > MAX_VISIBLE_COMMIT_LIST_COUNT) {
+ $systemNote.find('.note-text').addClass('system-note-commit-list');
+ $systemNote.find('.system-note-commit-list-toggler').show();
+ } else {
+ $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade');
+ }
+ });
+ };
+
+ return Notes;
+ })();
}).call(this);
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index ef3f2c6ae73..926dc35fee8 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */
+/* global Flash */
+
(function() {
this.NotificationsDropdown = (function() {
function NotificationsDropdown() {
@@ -17,7 +19,7 @@
});
$(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) {
if (data.saved) {
- return $(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html);
+ return $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html);
} else {
return new Flash('Failed to save new settings', 'alert');
}
@@ -25,7 +27,5 @@
}
return NotificationsDropdown;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 6fbec8efe9b..c3d7cc0adfb 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,6 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.NotificationsForm = (function() {
function NotificationsForm() {
@@ -53,7 +53,5 @@
};
return NotificationsForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
deleted file mode 100644
index 2e4dc62273e..00000000000
--- a/app/assets/javascripts/pager.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint-disable */
-(function() {
- this.Pager = {
- init: function(limit, preload, disable, callback) {
- this.limit = limit != null ? limit : 0;
- this.disable = disable != null ? disable : false;
- this.callback = callback != null ? callback : $.noop;
- this.loading = $('.loading').first();
- if (preload) {
- this.offset = 0;
- this.getOld();
- } else {
- this.offset = this.limit;
- }
- return this.initLoadMore();
- },
- getOld: function() {
- this.loading.show();
- return $.ajax({
- type: "GET",
- url: $(".content_list").data('href') || location.href,
- data: "limit=" + this.limit + "&offset=" + this.offset,
- complete: (function(_this) {
- return function() {
- return _this.loading.hide();
- };
- })(this),
- success: function(data) {
- Pager.append(data.count, data.html);
- return Pager.callback();
- },
- dataType: "json"
- });
- },
- append: function(count, html) {
- $(".content_list").append(html);
- if (count > 0) {
- return this.offset += count;
- } else {
- return this.disable = true;
- }
- },
- initLoadMore: function() {
- $(document).unbind('scroll');
- return $(document).endlessScroll({
- bottomPixels: 400,
- fireDelay: 1000,
- fireOnce: true,
- ceaseFire: function() {
- return Pager.disable;
- },
- callback: (function(_this) {
- return function(i) {
- if (!_this.loading.is(':visible')) {
- _this.loading.show();
- return Pager.getOld();
- }
- };
- })(this)
- });
- }
- };
-
-}).call(this);
diff --git a/app/assets/javascripts/pager.js.es6 b/app/assets/javascripts/pager.js.es6
new file mode 100644
index 00000000000..e35cf6d295e
--- /dev/null
+++ b/app/assets/javascripts/pager.js.es6
@@ -0,0 +1,73 @@
+(() => {
+ const ENDLESS_SCROLL_BOTTOM_PX = 400;
+ const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
+
+ const Pager = {
+ init(limit = 0, preload = false, disable = false, callback = $.noop) {
+ this.limit = limit;
+ this.offset = this.limit;
+ this.disable = disable;
+ this.callback = callback;
+ this.loading = $('.loading').first();
+ if (preload) {
+ this.offset = 0;
+ this.getOld();
+ }
+ this.initLoadMore();
+ },
+
+ getOld() {
+ this.loading.show();
+ $.ajax({
+ type: 'GET',
+ url: $('.content_list').data('href') || window.location.href,
+ data: `limit=${this.limit}&offset=${this.offset}`,
+ dataType: 'json',
+ error: () => this.loading.hide(),
+ success: (data) => {
+ this.append(data.count, data.html);
+ this.callback();
+
+ // keep loading until we've filled the viewport height
+ if (!this.disable && !this.isScrollable()) {
+ this.getOld();
+ } else {
+ this.loading.hide();
+ }
+ },
+ });
+ },
+
+ append(count, html) {
+ $('.content_list').append(html);
+ if (count > 0) {
+ this.offset += count;
+ } else {
+ this.disable = true;
+ }
+ },
+
+ isScrollable() {
+ const $w = $(window);
+ return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
+ },
+
+ initLoadMore() {
+ $(document).unbind('scroll');
+ $(document).endlessScroll({
+ bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
+ fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
+ fireOnce: true,
+ ceaseFire: () => this.disable === true,
+ callback: () => {
+ if (!this.loading.is(':visible')) {
+ this.loading.show();
+ this.getOld();
+ }
+ },
+ });
+ },
+ };
+
+ window.Pager = Pager;
+})();
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index e6fada5c84c..43263368494 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -1,29 +1,23 @@
-/* eslint-disable */
-((global) => {
+/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */
- class Pipelines {
- constructor() {
- this.initGraphToggle();
- this.addMarginToBuildColumns();
- }
+//= require lib/utils/bootstrap_linked_tabs
- initGraphToggle() {
- this.pipelineGraph = document.querySelector('.pipeline-graph');
- this.toggleButton = document.querySelector('.toggle-pipeline-btn');
- this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text');
- this.toggleButton.addEventListener('click', this.toggleGraph.bind(this));
- }
+((global) => {
+ class Pipelines {
+ constructor(options = {}) {
+ if (options.initTabs && options.tabsOptions) {
+ new global.LinkedTabs(options.tabsOptions);
+ }
- toggleGraph() {
- const graphCollapsed = this.pipelineGraph.classList.contains('graph-collapsed');
- this.toggleButton.classList.toggle('graph-collapsed');
- this.pipelineGraph.classList.toggle('graph-collapsed');
- this.toggleButtonText.textContent = graphCollapsed ? 'Hide' : 'Expand';
+ this.addMarginToBuildColumns();
}
addMarginToBuildColumns() {
+ this.pipelineGraph = document.querySelector('.js-pipeline-graph');
+
const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
- for (buildNodeIndex in secondChildBuildNodes) {
+
+ for (const buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling;
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
@@ -35,10 +29,10 @@
const columnBuilds = previousColumn.querySelectorAll('.build');
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
}
+
this.pipelineGraph.classList.remove('hidden');
}
}
global.Pipelines = Pipelines;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index f2a45a18bed..07eea98e737 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,13 +1,18 @@
-/* eslint-disable */
+/* eslint-disable func-names, no-var, object-shorthand, comma-dangle, prefer-arrow-callback */
+
// MarkdownPreview
//
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
// and showing a warning when more than `x` users are referenced.
//
-(function() {
- var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
-
- this.MarkdownPreview = (function() {
+(function () {
+ var lastTextareaPreviewed;
+ var lastTextareaHeight = null;
+ var markdownPreview;
+ var previewButtonSelector;
+ var writeButtonSelector;
+
+ window.MarkdownPreview = (function () {
function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning
@@ -15,75 +20,73 @@
MarkdownPreview.prototype.ajaxCache = {};
- MarkdownPreview.prototype.showPreview = function(form) {
- var mdText, preview;
- preview = form.find('.js-md-preview');
- mdText = form.find('textarea.markdown-area').val();
+ MarkdownPreview.prototype.showPreview = function ($form) {
+ var mdText;
+ var preview = $form.find('.js-md-preview');
+ if (preview.hasClass('md-preview-loading')) {
+ return;
+ }
+ mdText = $form.find('textarea.markdown-area').val();
+
if (mdText.trim().length === 0) {
preview.text('Nothing to preview.');
- return this.hideReferencedUsers(form);
+ this.hideReferencedUsers($form);
} else {
- preview.text('Loading...');
- return this.renderMarkdown(mdText, (function(_this) {
- return function(response) {
- preview.html(response.body);
- preview.syntaxHighlight();
- return _this.renderReferencedUsers(response.references.users, form);
- };
- })(this));
+ preview.addClass('md-preview-loading').text('Loading...');
+ this.fetchMarkdownPreview(mdText, (function (response) {
+ preview.removeClass('md-preview-loading').html(response.body);
+ preview.renderGFM();
+ this.renderReferencedUsers(response.references.users, $form);
+ }).bind(this));
}
};
- MarkdownPreview.prototype.renderMarkdown = function(text, success) {
+ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) {
if (!window.preview_markdown_path) {
return;
}
if (text === this.ajaxCache.text) {
- return success(this.ajaxCache.response);
+ success(this.ajaxCache.response);
+ return;
}
- return $.ajax({
+ $.ajax({
type: 'POST',
url: window.preview_markdown_path,
data: {
text: text
},
dataType: 'json',
- success: (function(_this) {
- return function(response) {
- _this.ajaxCache = {
- text: text,
- response: response
- };
- return success(response);
+ success: (function (response) {
+ this.ajaxCache = {
+ text: text,
+ response: response
};
- })(this)
+ success(response);
+ }).bind(this)
});
};
- MarkdownPreview.prototype.hideReferencedUsers = function(form) {
- var referencedUsers;
- referencedUsers = form.find('.referenced-users');
- return referencedUsers.hide();
+ MarkdownPreview.prototype.hideReferencedUsers = function ($form) {
+ $form.find('.referenced-users').hide();
};
- MarkdownPreview.prototype.renderReferencedUsers = function(users, form) {
+ MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) {
var referencedUsers;
- referencedUsers = form.find('.referenced-users');
+ referencedUsers = $form.find('.referenced-users');
if (referencedUsers.length) {
if (users.length >= this.referenceThreshold) {
referencedUsers.show();
- return referencedUsers.find('.js-referenced-users-count').text(users.length);
+ referencedUsers.find('.js-referenced-users-count').text(users.length);
} else {
- return referencedUsers.hide();
+ referencedUsers.hide();
}
}
};
return MarkdownPreview;
+ }());
- })();
-
- markdownPreview = new MarkdownPreview();
+ markdownPreview = new window.MarkdownPreview();
previewButtonSelector = '.js-md-preview-button';
@@ -91,71 +94,75 @@
lastTextareaPreviewed = null;
- $.fn.setupMarkdownPreview = function() {
- var $form, form_textarea;
- $form = $(this);
- form_textarea = $form.find('textarea.markdown-area');
- form_textarea.on('input', function() {
- return markdownPreview.hideReferencedUsers($form);
- });
- return form_textarea.on('blur', function() {
- return markdownPreview.showPreview($form);
+ $.fn.setupMarkdownPreview = function () {
+ var $form = $(this);
+ $form.find('textarea.markdown-area').on('input', function () {
+ markdownPreview.hideReferencedUsers($form);
});
};
- $(document).on('markdown-preview:show', function(e, $form) {
+ $(document).on('markdown-preview:show', function (e, $form) {
if (!$form) {
return;
}
+
lastTextareaPreviewed = $form.find('textarea.markdown-area');
+ lastTextareaHeight = lastTextareaPreviewed.height();
+
// toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active');
$form.find(previewButtonSelector).parent().addClass('active');
+
// toggle content
$form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show();
- return markdownPreview.showPreview($form);
+ markdownPreview.showPreview($form);
});
- $(document).on('markdown-preview:hide', function(e, $form) {
+ $(document).on('markdown-preview:hide', function (e, $form) {
if (!$form) {
return;
}
lastTextareaPreviewed = null;
+
+ if (lastTextareaHeight) {
+ $form.find('textarea.markdown-area').height(lastTextareaHeight);
+ }
+
// toggle tabs
$form.find(writeButtonSelector).parent().addClass('active');
$form.find(previewButtonSelector).parent().removeClass('active');
+
// toggle content
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
- return $form.find('.md-preview-holder').hide();
+ $form.find('.md-preview-holder').hide();
});
- $(document).on('markdown-preview:toggle', function(e, keyboardEvent) {
+ $(document).on('markdown-preview:toggle', function (e, keyboardEvent) {
var $target;
$target = $(keyboardEvent.target);
if ($target.is('textarea.markdown-area')) {
$(document).triggerHandler('markdown-preview:show', [$target.closest('form')]);
- return keyboardEvent.preventDefault();
+ keyboardEvent.preventDefault();
} else if (lastTextareaPreviewed) {
$target = lastTextareaPreviewed;
$(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]);
- return keyboardEvent.preventDefault();
+ keyboardEvent.preventDefault();
}
});
- $(document).on('click', previewButtonSelector, function(e) {
+ $(document).on('click', previewButtonSelector, function (e) {
var $form;
e.preventDefault();
$form = $(this).closest('form');
- return $(document).triggerHandler('markdown-preview:show', [$form]);
+ $(document).triggerHandler('markdown-preview:show', [$form]);
});
- $(document).on('click', writeButtonSelector, function(e) {
+ $(document).on('click', writeButtonSelector, function (e) {
var $form;
e.preventDefault();
$form = $(this).closest('form');
- return $(document).triggerHandler('markdown-preview:hide', [$form]);
+ $(document).triggerHandler('markdown-preview:hide', [$form]);
});
-
-}).call(this);
+}());
diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6
index 6da6c1d0295..42e9847af91 100644
--- a/app/assets/javascripts/profile/gl_crop.js.es6
+++ b/app/assets/javascripts/profile/gl_crop.js.es6
@@ -1,13 +1,12 @@
-/* eslint-disable */
-((global) => {
+/* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */
+((global) => {
// Matches everything but the file name
const FILENAMEREGEX = /^.*[\\\/]/;
class GitLabCrop {
constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
-
this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
this.onModalHide = this.onModalHide.bind(this);
this.onModalShow = this.onModalShow.bind(this);
@@ -135,7 +134,7 @@
var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]);
array = [];
- for (k = i = 0, len = binary.length; i < len; k = ++i) {
+ for (k = i = 0, len = binary.length; i < len; k = (i += 1)) {
v = binary[k];
array.push(binary.charCodeAt(k));
}
@@ -168,6 +167,5 @@
return this.each(function() {
return $(this).data('glcrop', new GitLabCrop(this, opts));
});
- }
-
+ };
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
index 73858388261..6dbaae25f2a 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
-((global) => {
+/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
+/* global Flash */
+((global) => {
class Profile {
constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this);
@@ -35,20 +36,16 @@
}
onSubmitForm(e) {
- e.preventDefault();
return this.saveForm();
}
beforeUpdateUsername() {
- $('.loading-username').show();
- $(this).find('.update-success').hide();
- return $(this).find('.update-failed').hide();
+ $('.loading-username', this).removeClass('hidden');
}
afterUpdateUsername() {
- $('.loading-username').hide();
- $(this).find('.btn-save').enable();
- return $(this).find('.loading-gif').hide();
+ $('.loading-username', this).addClass('hidden');
+ $('button[type=submit]', this).enable();
}
onUpdateNotifs(e, data) {
@@ -97,5 +94,4 @@
return new Profile();
}
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index 22bee0f6187..f50802bdf2e 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require_tree . */
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 2d0c6b16699..7cf630a1d76 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
+/* global Cookies */
+/* global Turbolinks */
+/* global ProjectSelect */
+
(function() {
this.Project = (function() {
function Project() {
@@ -90,11 +94,11 @@
return $el.text().trim();
},
clicked: function(selected, $el, e) {
- e.preventDefault()
+ e.preventDefault();
if ($('input[name="ref"]').length) {
- var $form = $dropdown.closest('form'),
- action = $form.attr('action'),
- divider = action.indexOf('?') < 0 ? '?' : '&';
+ var $form = $dropdown.closest('form');
+ var action = $form.attr('action');
+ var divider = action.indexOf('?') < 0 ? '?' : '&';
Turbolinks.visit(action + '' + divider + '' + $form.serialize());
}
}
@@ -103,7 +107,5 @@
};
return Project;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index 61877c6616d..a6d3ba9eb86 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
(function() {
this.ProjectAvatar = (function() {
function ProjectAvatar() {
@@ -16,7 +16,5 @@
}
return ProjectAvatar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index ddac5ed83e1..04fe84683f3 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,6 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */
+/* global fuzzaldrinPlus */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.ProjectFindFile = (function() {
var highlighter;
@@ -69,7 +71,7 @@
var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty();
results = [];
- for (i = j = 0, len = filePaths.length; j < len; i = ++j) {
+ for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) {
filePath = filePaths[i];
if (i === 20) {
break;
@@ -90,7 +92,7 @@
lastIndex = 0;
highlightText = "";
matchedChars = [];
- for (j = 0, len = matches.length; j < len; j++) {
+ for (j = 0, len = matches.length; j < len; j += 1) {
matchIndex = matches[j];
unmatched = text.substring(lastIndex, matchIndex);
if (unmatched) {
@@ -165,7 +167,5 @@
};
return ProjectFindFile;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index fd95f8f2c19..208f25a0e33 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
(function() {
this.ProjectFork = (function() {
function ProjectFork() {
@@ -9,7 +9,5 @@
}
return ProjectFork;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index f1c4a9fe542..6614d8952cd 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
+/* global Turbolinks */
+
(function() {
this.ProjectImport = (function() {
function ProjectImport() {
@@ -8,7 +10,5 @@
}
return ProjectImport;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6
new file mode 100644
index 00000000000..8365f7118d5
--- /dev/null
+++ b/app/assets/javascripts/project_label_subscription.js.es6
@@ -0,0 +1,53 @@
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */
+
+(function(global) {
+ class ProjectLabelSubscription {
+ constructor(container) {
+ this.$container = $(container);
+ this.$buttons = this.$container.find('.js-subscribe-button');
+
+ this.$buttons.on('click', this.toggleSubscription.bind(this));
+ }
+
+ toggleSubscription(event) {
+ event.preventDefault();
+
+ const $btn = $(event.currentTarget);
+ const $span = $btn.find('span');
+ const url = $btn.attr('data-url');
+ const oldStatus = $btn.attr('data-status');
+
+ $btn.addClass('disabled');
+ $span.toggleClass('hidden');
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ let newStatus, newAction;
+
+ if (oldStatus === 'unsubscribed') {
+ [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
+ } else {
+ [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
+ }
+
+ $span.toggleClass('hidden');
+ $btn.removeClass('disabled');
+
+ this.$buttons.attr('data-status', newStatus);
+ this.$buttons.find('> span').text(newAction);
+
+ for (const button of this.$buttons) {
+ const $button = $(button);
+
+ if ($button.attr('data-original-title')) {
+ $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
+ }
+ }
+ });
+ }
+ }
+
+ global.ProjectLabelSubscription = ProjectLabelSubscription;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 0d3fb31a9cf..3aa6f6771ce 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.ProjectNew = (function() {
function ProjectNew() {
@@ -14,18 +15,29 @@
return $('.save-project-loader').show();
};
})(this));
+
+ this.initVisibilitySelect();
+
this.toggleSettings();
this.toggleSettingsOnclick();
this.toggleRepoVisibility();
}
+ ProjectNew.prototype.initVisibilitySelect = function() {
+ const visibilityContainer = document.querySelector('.js-visibility-select');
+ if (!visibilityContainer) return;
+ const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
+ visibilitySelect.init();
+ };
+
ProjectNew.prototype.toggleSettings = function() {
var self = this;
this.$selects.each(function () {
- var $select = $(this),
- className = $select.data('field').replace(/_/g, '-')
- .replace('access-level', 'feature');
+ var $select = $(this);
+ var className = $select.data('field')
+ .replace(/_/g, '-')
+ .replace('access-level', 'feature');
self._showOrHide($select, '.' + className);
});
};
@@ -45,9 +57,9 @@
};
ProjectNew.prototype.toggleRepoVisibility = function () {
- var $repoAccessLevel = $('.js-repo-access-level select'),
- containerRegistry = document.querySelectorAll('.js-container-registry')[0],
- containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
+ var $repoAccessLevel = $('.js-repo-access-level select');
+ var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
+ var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
@@ -55,11 +67,11 @@
$repoAccessLevel.off('change')
.on('change', function () {
- var selectedVal = parseInt($repoAccessLevel.val());
+ var selectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.each(function () {
- var $this = $(this),
- repoSelectVal = parseInt($this.val());
+ var $this = $(this);
+ var repoSelectVal = parseInt($this.val(), 10);
$this.find('option').show();
@@ -88,7 +100,5 @@
};
return ProjectNew;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index e1acf3c8232..7b5e9953598 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
+/* global Api */
+
(function() {
this.ProjectSelect = (function() {
function ProjectSelect() {
@@ -13,6 +15,7 @@
},
data: function(term, callback) {
var finalCallback, projectsCallback;
+ var orderBy = $dropdown.data('order-by');
finalCallback = function(projects) {
return callback(projects);
};
@@ -32,7 +35,7 @@
if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback);
} else {
- return Api.projects(term, this.orderBy, projectsCallback);
+ return Api.projects(term, orderBy, projectsCallback);
}
},
url: function(project) {
@@ -97,7 +100,5 @@
}
return ProjectSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index 21650f5f67a..aad130cf267 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -1,12 +1,11 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife */
+
(function() {
this.ProjectShow = (function() {
function ProjectShow() {}
return ProjectShow;
-
})();
-
}).call(this);
// I kept class for future
diff --git a/app/assets/javascripts/project_variables.js.es6 b/app/assets/javascripts/project_variables.js.es6
new file mode 100644
index 00000000000..4ee2e49306d
--- /dev/null
+++ b/app/assets/javascripts/project_variables.js.es6
@@ -0,0 +1,43 @@
+(() => {
+ const HIDDEN_VALUE_TEXT = '******';
+
+ class ProjectVariables {
+ constructor() {
+ this.$revealBtn = $('.js-btn-toggle-reveal-values');
+ this.$revealBtn.on('click', this.toggleRevealState.bind(this));
+ }
+
+ toggleRevealState(e) {
+ e.preventDefault();
+
+ const oldStatus = this.$revealBtn.attr('data-status');
+ let newStatus = 'hidden';
+ let newAction = 'Reveal Values';
+
+ if (oldStatus === 'hidden') {
+ newStatus = 'revealed';
+ newAction = 'Hide Values';
+ }
+
+ this.$revealBtn.attr('data-status', newStatus);
+
+ const $variables = $('.variable-value');
+
+ $variables.each((_, variable) => {
+ const $variable = $(variable);
+ let newText = HIDDEN_VALUE_TEXT;
+
+ if (newStatus === 'revealed') {
+ newText = $variable.attr('data-value');
+ }
+
+ $variable.text(newText);
+ });
+
+ this.$revealBtn.text(newAction);
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.ProjectVariables = ProjectVariables;
+})();
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index 3458cd89ae2..69a11dfaf39 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, max-len */
+
(function() {
- this.ProjectsList = {
+ window.ProjectsList = {
init: function() {
$(".projects-list-filter").off('keyup');
this.initSearch();
@@ -9,7 +10,7 @@
initSearch: function() {
var debounceFilter, projectsListFilter;
projectsListFilter = $('.projects-list-filter');
- debounceFilter = _.debounce(ProjectsList.filterResults, 500);
+ debounceFilter = _.debounce(window.ProjectsList.filterResults, 500);
return projectsListFilter.on('keyup', function(e) {
if (projectsListFilter.val() !== '') {
return debounceFilter();
@@ -46,5 +47,4 @@
});
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
index 2d60947a666..e7fff57ff45 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, max-len */
+
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchAccessDropdown = class {
constructor(options) {
@@ -24,6 +25,5 @@
}
});
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
index c45c9d8ff22..57ea2f52814 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
@@ -1,6 +1,8 @@
-/* eslint-disable */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
+/* global ProtectedBranchDropdown */
+
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchCreate = class {
constructor() {
@@ -42,7 +44,6 @@
// This will run after clicked callback
onSelect() {
-
// Enable submit button
const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
@@ -50,6 +51,5 @@
this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
index e3f226e9a2a..03f4531abf5 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars */
+
class ProtectedBranchDropdown {
constructor(options) {
this.onSelect = options.onSelect;
@@ -75,3 +76,5 @@ class ProtectedBranchDropdown {
this.$dropdownFooter.toggleClass('hidden', !branchName);
}
}
+
+window.ProtectedBranchDropdown = ProtectedBranchDropdown;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
index ac3142ffb07..149e511451e 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
@@ -1,6 +1,8 @@
-/* eslint-disable */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
+/* global Flash */
+
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchEdit = class {
constructor(options) {
@@ -12,7 +14,6 @@
}
buildDropdowns() {
-
// Allowed to merge dropdown
new gl.ProtectedBranchAccessDropdown({
$dropdown: this.$allowedToMergeDropdown,
@@ -33,7 +34,7 @@
const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
// Do not update if one dropdown has not selected any option
- if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
+ if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
$.ajax({
type: 'POST',
@@ -61,6 +62,5 @@
}
});
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
index 705378a364d..336fa6c57a7 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
@@ -1,9 +1,10 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, no-new, comma-dangle */
+
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchEditList = class {
- constructor() {
+ constructor() {
this.$wrap = $('.protected-branches-list');
// Build edit forms
@@ -13,6 +14,5 @@
});
});
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
index 17e34163831..15b3affd469 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1,2 +1 @@
-/* eslint-disable */
/*= require_tree . */
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
new file mode 100644
index 00000000000..0caf8ba4344
--- /dev/null
+++ b/app/assets/javascripts/render_gfm.js
@@ -0,0 +1,15 @@
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len */
+// Render Gitlab flavoured Markdown
+//
+// Delegates to syntax highlight and render math
+//
+(function() {
+ $.fn.renderGFM = function() {
+ this.find('.js-syntax-highlight').syntaxHighlight();
+ this.find('.js-render-math').renderMath();
+ };
+
+ $(document).on('ready page:load', function() {
+ return $('body').renderGFM();
+ });
+}).call(this);
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
new file mode 100644
index 00000000000..6cef449babf
--- /dev/null
+++ b/app/assets/javascripts/render_math.js
@@ -0,0 +1,54 @@
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len, no-console */
+// Renders math using KaTeX in any element with the
+// `js-render-math` class
+//
+// ### Example Markup
+//
+// <code class="js-render-math"></div>
+//
+(function() {
+ // Only load once
+ var katexLoaded = false;
+
+ // Loop over all math elements and render math
+ var renderWithKaTeX = function (elements) {
+ elements.each(function () {
+ var mathNode = $('<span></span>');
+ var $this = $(this);
+
+ var display = $this.attr('data-math-style') === 'display';
+ try {
+ katex.render($this.text(), mathNode.get(0), { displayMode: display });
+ mathNode.insertAfter($this);
+ $this.remove();
+ } catch (err) {
+ // What can we do??
+ console.log(err.message);
+ }
+ });
+ };
+
+ $.fn.renderMath = function() {
+ var $this = this;
+ if ($this.length === 0) return;
+
+ if (katexLoaded) renderWithKaTeX($this);
+ else {
+ // Request CSS file so it is in the cache
+ $.get(gon.katex_css_url, function() {
+ var css = $('<link>',
+ { rel: 'stylesheet',
+ type: 'text/css',
+ href: gon.katex_css_url,
+ });
+ css.appendTo('head');
+
+ // Load KaTeX js
+ $.getScript(gon.katex_js_url, function() {
+ katexLoaded = true;
+ renderWithKaTeX($this); // Run KaTeX
+ });
+ });
+ }
+ };
+}).call(this);
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index df38937858f..76a0f993ea0 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,6 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
+/* global Cookies */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Sidebar = (function() {
function Sidebar(currentUser) {
@@ -16,7 +18,7 @@
$('.dropdown').off('loading.gl.dropdown');
$('.dropdown').off('loaded.gl.dropdown');
$(document).off('click', '.js-sidebar-toggle');
- }
+ };
Sidebar.prototype.addEventListeners = function() {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
@@ -198,7 +200,5 @@
};
return Sidebar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index d79e6f014f6..489e567259c 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
+/* global Api */
+
(function() {
this.Search = (function() {
function Search() {
@@ -10,6 +12,9 @@
selectable: true,
filterable: true,
fieldName: 'group_id',
+ search: {
+ fields: ['name']
+ },
data: function(term, callback) {
return Api.groups(term, {}, function(data) {
data.unshift({
@@ -38,6 +43,9 @@
selectable: true,
filterable: true,
fieldName: 'project_id',
+ search: {
+ fields: ['name']
+ },
data: function(term, callback) {
return Api.projects(term, 'id', function(data) {
data.unshift({
@@ -88,7 +96,5 @@
};
return Search;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6
index 5fa94556501..480755899fb 100644
--- a/app/assets/javascripts/search_autocomplete.js.es6
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -1,6 +1,6 @@
-/* eslint-disable */
-((global) => {
+/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
+((global) => {
const KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
@@ -104,7 +104,7 @@
data = [];
// List results
firstCategory = true;
- for (i = 0, len = response.length; i < len; i++) {
+ for (i = 0, len = response.length; i < len; i += 1) {
suggestion = response[i];
// Add group header before list each group
if (lastCategory !== suggestion.category) {
@@ -141,8 +141,9 @@
}
getCategoryContents() {
- var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
+ var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils;
userId = gon.current_user_id;
+ userName = gon.current_username;
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
if (utils.isInGroupsPage() && groupOptions) {
options = groupOptions[utils.getGroupSlug()];
@@ -157,10 +158,10 @@
header: "" + name
}, {
text: 'Issues assigned to me',
- url: issuesPath + "/?assignee_id=" + userId
+ url: issuesPath + "/?assignee_username=" + userName
}, {
text: "Issues I've created",
- url: issuesPath + "/?author_id=" + userId
+ url: issuesPath + "/?author_username=" + userName
}, 'separator', {
text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_id=" + userId
@@ -214,7 +215,7 @@
this.dropdown.addClass('open').trigger('shown.bs.dropdown');
return this.searchInput.removeClass('disabled');
}
- };
+ }
// Saves last length of the entered text
onSearchInputKeyDown() {
@@ -278,12 +279,12 @@
return this.searchInput.val();
}
- onClearInputClick(e) {
+ onClearInputClick(e) {
e.preventDefault();
return this.searchInput.val('').focus();
}
- onSearchInputBlur(e) {
+ onSearchInputBlur(e) {
this.isFocused = false;
this.wrap.removeClass('search-active');
// If input is blank then restore state
@@ -303,12 +304,12 @@
hasLocationBadge() {
return this.wrap.is('.has-location-badge');
- };
+ }
restoreOriginalState() {
var i, input, inputs, len;
inputs = Object.keys(this.originalState);
- for (i = 0, len = inputs.length; i < len; i++) {
+ for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
this.getElement("#" + input).val(this.originalState[input]);
}
@@ -329,7 +330,7 @@
var i, input, inputs, len, results;
inputs = Object.keys(this.originalState);
results = [];
- for (i = 0, len = inputs.length; i < len; i++) {
+ for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
// _location isnt a input
if (input === '_location') {
@@ -359,7 +360,7 @@
var html;
html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
return this.dropdownContent.html(html);
- };
+ }
onClick(item, $el, e) {
if (location.pathname.indexOf(item.url) !== -1) {
@@ -382,8 +383,7 @@
this.disableAutocomplete();
return this.searchInput.val('').focus();
}
- };
-
+ }
}
global.SearchAutocomplete = SearchAutocomplete;
@@ -424,5 +424,4 @@
};
}
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 8d8ab6dda5e..c56ee429b8e 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,6 +1,10 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
+/* global Mousetrap */
+/* global Turbolinks */
+/* global findFileURL */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Shortcuts = (function() {
function Shortcuts(skipResetBindings) {
@@ -47,7 +51,7 @@
var i, l, len, results;
if (location && location.length > 0) {
results = [];
- for (i = 0, len = location.length; i < len; i++) {
+ for (i = 0, len = location.length; i < len; i += 1) {
l = location[i];
results.push($(l).show());
}
@@ -74,7 +78,6 @@
};
return Shortcuts;
-
})();
$(document).on('click.more_help', '.js-more-help-button', function(e) {
@@ -95,5 +98,4 @@
}
};
})();
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index 704a8bd3a57..d50ddd98de1 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return */
+/* global Shortcuts */
+/* global Mousetrap */
/*= require shortcuts */
@@ -23,7 +25,5 @@
};
return ShortcutsBlob;
-
})(Shortcuts);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index befe4eccdba..603fefbf15a 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
+/* global Mousetrap */
+/* global Shortcuts */
/*= require shortcuts */
@@ -34,7 +36,5 @@
};
return ShortcutsDashboardNavigation;
-
})(Shortcuts);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 90ed4267661..8469837533b 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife */
+/* global Mousetrap */
+/* global ShortcutsNavigation */
/*= require shortcuts_navigation */
@@ -32,7 +34,5 @@
}
return ShortcutsFindFile;
-
})(ShortcutsNavigation);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 25ec7dbc067..6f919de3da7 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */
+/* global Mousetrap */
+/* global Turbolinks */
+/* global ShortcutsNavigation */
+/* global sidebar */
/*= require mousetrap */
/*= require shortcuts_navigation */
@@ -72,7 +76,5 @@
};
return ShortcutsIssuable;
-
})(ShortcutsNavigation);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 19c6b7d30ab..afeda0dd5fe 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
+/* global Mousetrap */
+/* global Shortcuts */
/*= require shortcuts */
@@ -62,7 +64,5 @@
};
return ShortcutsNavigation;
-
})(Shortcuts);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index 002e979a2c6..79896e35cbb 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, max-len */
+/* global Mousetrap */
+/* global ShortcutsNavigation */
/*= require shortcuts_navigation */
@@ -22,7 +24,5 @@
}
return ShortcutsNetwork;
-
})(ShortcutsNavigation);
-
}).call(this);
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
index a23ca449c4b..05234643c18 100644
--- a/app/assets/javascripts/sidebar.js.es6
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
+/* global Cookies */
+
((global) => {
let singleton;
@@ -92,5 +94,4 @@
}
global.Sidebar = Sidebar;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/signin_tabs_memoizer.js.es6 b/app/assets/javascripts/signin_tabs_memoizer.js.es6
new file mode 100644
index 00000000000..d811d1cd53a
--- /dev/null
+++ b/app/assets/javascripts/signin_tabs_memoizer.js.es6
@@ -0,0 +1,49 @@
+/* eslint no-param-reassign: ["error", { "props": false }]*/
+/* eslint no-new: "off" */
+((global) => {
+ /**
+ * Memorize the last selected tab after reloading a page.
+ * Does that setting the current selected tab in the localStorage
+ */
+ class ActiveTabMemoizer {
+ constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
+ this.currentTabKey = currentTabKey;
+ this.tabSelector = tabSelector;
+ this.bootstrap();
+ }
+
+ bootstrap() {
+ const tabs = document.querySelectorAll(this.tabSelector);
+ if (tabs.length > 0) {
+ tabs[0].addEventListener('click', (e) => {
+ if (e.target && e.target.nodeName === 'A') {
+ const anchorName = e.target.getAttribute('href');
+ this.saveData(anchorName);
+ }
+ });
+ }
+
+ this.showTab();
+ }
+
+ showTab() {
+ const anchorName = this.readData();
+ if (anchorName) {
+ const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`);
+ if (tab) {
+ tab.click();
+ }
+ }
+ }
+
+ saveData(val) {
+ localStorage.setItem(this.currentTabKey, val);
+ }
+
+ readData() {
+ return localStorage.getItem(this.currentTabKey);
+ }
+ }
+
+ global.ActiveTabMemoizer = ActiveTabMemoizer;
+})(window);
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 8e54ca4f0dc..5b20c63384c 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,8 +1,9 @@
-/* eslint-disable */
+/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
- this.SingleFileDiff = (function() {
+ window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
WRAPPER = '<div class="diff-content diff-wrap-lines"></div>';
@@ -13,7 +14,7 @@
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
- function SingleFileDiff(file, forceLoad, cb) {
+ function SingleFileDiff(file) {
this.file = file;
this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file);
@@ -31,29 +32,28 @@
this.content.after(this.collapsedContent);
this.$toggleIcon.addClass('fa-caret-down');
}
- $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
- if (forceLoad) {
- this.toggleDiff(null, cb);
- }
+
+ $('.file-title, .click-to-expand', this.file).on('click', (function (e) {
+ this.toggleDiff($(e.target));
+ }).bind(this));
}
- SingleFileDiff.prototype.toggleDiff = function(e, cb) {
- var $target = $(e.target);
+ SingleFileDiff.prototype.toggleDiff = function($target, cb) {
if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
@@ -76,8 +76,8 @@
}
_this.collapsedContent.after(_this.content);
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
if (cb) cb();
@@ -86,15 +86,13 @@
};
return SingleFileDiff;
-
})();
- $.fn.singleFileDiff = function(forceLoad, cb) {
+ $.fn.singleFileDiff = function() {
return this.each(function() {
- if (!$.data(this, 'singleFileDiff') || forceLoad) {
- return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb));
+ if (!$.data(this, 'singleFileDiff')) {
+ return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this));
}
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6
new file mode 100644
index 00000000000..40f67637c7c
--- /dev/null
+++ b/app/assets/javascripts/smart_interval.js.es6
@@ -0,0 +1,157 @@
+/*
+* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
+* and controllable by a public API.
+*
+* */
+
+(() => {
+ class SmartInterval {
+ /**
+ * @param { function } opts.callback Function to be called on each iteration (required)
+ * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
+ * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
+ * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
+ * when the page is hidden
+ * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
+ * @param { boolean } opts.lazyStart Configure if timer is initialized on
+ * instantiation or lazily
+ * @param { boolean } opts.immediateExecution Configure if callback should
+ * be executed before the first interval.
+ */
+ constructor(opts = {}) {
+ this.cfg = {
+ callback: opts.callback,
+ startingInterval: opts.startingInterval,
+ maxInterval: opts.maxInterval,
+ hiddenInterval: opts.hiddenInterval,
+ incrementByFactorOf: opts.incrementByFactorOf,
+ lazyStart: opts.lazyStart,
+ immediateExecution: opts.immediateExecution,
+ };
+
+ this.state = {
+ intervalId: null,
+ currentInterval: this.cfg.startingInterval,
+ pageVisibility: 'visible',
+ };
+
+ this.initInterval();
+ }
+ /* public */
+
+ start() {
+ const cfg = this.cfg;
+ const state = this.state;
+
+ if (cfg.immediateExecution) {
+ cfg.immediateExecution = false;
+ cfg.callback();
+ }
+
+ state.intervalId = window.setInterval(() => {
+ cfg.callback();
+
+ if (this.getCurrentInterval() === cfg.maxInterval) {
+ return;
+ }
+
+ this.incrementInterval();
+ this.resume();
+ }, this.getCurrentInterval());
+ }
+
+ // cancel the existing timer, setting the currentInterval back to startingInterval
+ cancel() {
+ this.setCurrentInterval(this.cfg.startingInterval);
+ this.stopTimer();
+ }
+
+ onVisibilityHidden() {
+ if (this.cfg.hiddenInterval) {
+ this.setCurrentInterval(this.cfg.hiddenInterval);
+ this.resume();
+ } else {
+ this.cancel();
+ }
+ }
+
+ // start a timer, using the existing interval
+ resume() {
+ this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
+ this.start();
+ }
+
+ onVisibilityVisible() {
+ this.cancel();
+ this.start();
+ }
+
+ destroy() {
+ this.cancel();
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
+ $(document).off('visibilitychange').off('page:before-unload');
+ }
+
+ /* private */
+
+ initInterval() {
+ const cfg = this.cfg;
+
+ if (!cfg.lazyStart) {
+ this.start();
+ }
+
+ this.initVisibilityChangeHandling();
+ this.initPageUnloadHandling();
+ }
+
+ initVisibilityChangeHandling() {
+ // cancel interval when tab no longer shown (prevents cached pages from polling)
+ document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
+ }
+
+ initPageUnloadHandling() {
+ // prevent interval continuing after page change, when kept in cache by Turbolinks
+ $(document).on('page:before-unload', () => this.cancel());
+ }
+
+ handleVisibilityChange(e) {
+ this.state.pageVisibility = e.target.visibilityState;
+ const intervalAction = this.isPageVisible() ?
+ this.onVisibilityVisible :
+ this.onVisibilityHidden;
+
+ intervalAction.apply(this);
+ }
+
+ getCurrentInterval() {
+ return this.state.currentInterval;
+ }
+
+ setCurrentInterval(newInterval) {
+ this.state.currentInterval = newInterval;
+ }
+
+ incrementInterval() {
+ const cfg = this.cfg;
+ const currentInterval = this.getCurrentInterval();
+ if (cfg.hiddenInterval && !this.isPageVisible()) return;
+ let nextInterval = currentInterval * cfg.incrementByFactorOf;
+
+ if (nextInterval > cfg.maxInterval) {
+ nextInterval = cfg.maxInterval;
+ }
+
+ this.setCurrentInterval(nextInterval);
+ }
+
+ isPageVisible() { return this.state.pageVisibility === 'visible'; }
+
+ stopTimer() {
+ const state = this.state;
+
+ state.intervalId = window.clearInterval(state.intervalId);
+ }
+ }
+ gl.SmartInterval = SmartInterval;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 083dc23c796..cfb4ff82a73 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,13 +1,14 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
+/* global ace */
+
/*= require_tree . */
(function() {
$(function() {
- var editor = ace.edit("editor")
+ var editor = ace.edit("editor");
$(".snippet-form-holder form").on('submit', function() {
$(".snippet-file-content").val(editor.getValue());
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6
index c3afc3f2246..2128007113f 100644
--- a/app/assets/javascripts/snippets_list.js.es6
+++ b/app/assets/javascripts/snippets_list.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */
+
(global => {
global.gl = global.gl || {};
@@ -8,5 +9,5 @@
$holder.find('.pagination').on('ajax:success', (e, data) => {
$holder.replaceWith(data.html);
});
- }
+ };
})(window);
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index cfd1e2204d5..531fd0e9c32 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
+/* global Flash */
+
(function() {
this.Star = (function() {
function Star() {
@@ -24,7 +26,5 @@
}
return Star;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6
new file mode 100644
index 00000000000..d8191605128
--- /dev/null
+++ b/app/assets/javascripts/subbable_resource.js.es6
@@ -0,0 +1,51 @@
+(() => {
+/*
+* SubbableResource can be extended to provide a pubsub-style service for one-off REST
+* calls. Subscribe by passing a callback or render method you will use to handle responses.
+ *
+* */
+
+ class SubbableResource {
+ constructor(resourcePath) {
+ this.endpoint = resourcePath;
+
+ // TODO: Switch to axios.create
+ this.resource = $.ajax;
+ this.subscribers = [];
+ }
+
+ subscribe(callback) {
+ this.subscribers.push(callback);
+ }
+
+ publish(newResponse) {
+ const responseCopy = _.extend({}, newResponse);
+ this.subscribers.forEach((fn) => {
+ fn(responseCopy);
+ });
+ return newResponse;
+ }
+
+ get(payload) {
+ return this.resource(payload)
+ .then(data => this.publish(data));
+ }
+
+ post(payload) {
+ return this.resource(payload)
+ .then(data => this.publish(data));
+ }
+
+ put(payload) {
+ return this.resource(payload)
+ .then(data => this.publish(data));
+ }
+
+ delete(payload) {
+ return this.resource(payload)
+ .then(data => this.publish(data));
+ }
+ }
+
+ gl.SubbableResource = SubbableResource;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
deleted file mode 100644
index f9915593657..00000000000
--- a/app/assets/javascripts/subscription.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/* eslint-disable */
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.Subscription = (function() {
- function Subscription(container) {
- this.toggleSubscription = bind(this.toggleSubscription, this);
- var $container;
- this.$container = $(container);
- this.url = this.$container.attr('data-url');
- this.subscribe_button = this.$container.find('.js-subscribe-button');
- this.subscription_status = this.$container.find('.subscription-status');
- this.subscribe_button.unbind('click').click(this.toggleSubscription);
- }
-
- Subscription.prototype.toggleSubscription = function(event) {
- var action, btn, current_status;
- btn = $(event.currentTarget);
- action = btn.find('span').text();
- current_status = this.subscription_status.attr('data-status');
- btn.addClass('disabled');
-
- if ($('html').hasClass('issue-boards-page')) {
- this.url = this.$container.attr('data-url');
- }
-
- return $.post(this.url, (function(_this) {
- return function() {
- var status;
- btn.removeClass('disabled');
-
- if ($('html').hasClass('issue-boards-page')) {
- Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed);
- } else {
- status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
- _this.subscription_status.attr('data-status', status);
- action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
- btn.find('span').text(action);
- _this.subscription_status.find('>div').toggleClass('hidden');
- if (btn.attr('data-original-title')) {
- return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
- }
- }
- };
- })(this));
- };
-
- return Subscription;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6
new file mode 100644
index 00000000000..62d1604fe9e
--- /dev/null
+++ b/app/assets/javascripts/subscription.js.es6
@@ -0,0 +1,50 @@
+/* global Vue */
+
+(() => {
+ class Subscription {
+ constructor(containerElm) {
+ this.containerElm = containerElm;
+
+ const subscribeButton = containerElm.querySelector('.js-subscribe-button');
+ if (subscribeButton) {
+ // remove class so we don't bind twice
+ subscribeButton.classList.remove('js-subscribe-button');
+ subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
+ }
+ }
+
+ toggleSubscription(event) {
+ const button = event.currentTarget;
+ const buttonSpan = button.querySelector('span');
+ if (!buttonSpan || button.classList.contains('disabled')) {
+ return;
+ }
+ button.classList.add('disabled');
+
+ const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
+ const toggleActionUrl = this.containerElm.dataset.url;
+
+ $.post(toggleActionUrl, () => {
+ button.classList.remove('disabled');
+
+ // hack to allow this to work with the issue boards Vue object
+ if (document.querySelector('html').classList.contains('issue-boards-page')) {
+ Vue.set(
+ gl.issueBoards.BoardsStore.detail.issue,
+ 'subscribed',
+ !gl.issueBoards.BoardsStore.detail.issue.subscribed,
+ );
+ } else {
+ buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
+ }
+ });
+ }
+
+ static bindAll(selector) {
+ [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.Subscription = Subscription;
+})();
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 2ca65cb762d..187356f0bf9 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.SubscriptionSelect = (function() {
function SubscriptionSelect() {
@@ -30,7 +30,5 @@
}
return SubscriptionSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 77ad4f30b7a..115716bff6a 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
+
// Syntax Highlighter
//
// Applies a syntax highlighting color scheme CSS class to any element with the
@@ -11,6 +12,7 @@
(function() {
$.fn.syntaxHighlight = function() {
var $children;
+
if ($(this).hasClass('js-syntax-highlight')) {
// Given the element itself, apply highlighting
return $(this).addClass(gon.user_color_scheme);
@@ -22,9 +24,4 @@
}
}
};
-
- $(document).on('ready page:load', function() {
- return $('.js-syntax-highlight').syntaxHighlight();
- });
-
}).call(this);
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index 93a3d67ee9f..b0132af70f2 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
+/* global Api */
+
/*= require ../blob/template_selector */
((global) => {
@@ -10,7 +12,7 @@
this.issuableType = this.wrapper.data('issuable-type');
this.titleInput = $(`#${this.issuableType}_title`);
- let initialQuery = {
+ const initialQuery = {
name: this.dropdown.data('selected')
};
@@ -21,7 +23,7 @@
});
$('.no-template', this.dropdown.parent()).on('click', () => {
- this.currentTemplate = '';
+ this.currentTemplate.content = '';
this.setInputValueToTemplateContent();
$('.dropdown-toggle-text', this.dropdown).text('Choose a template');
});
@@ -45,10 +47,10 @@
// If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the
// `skipFocus` option to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, {skipFocus: true});
+ this.requestFileSuccess(this.currentTemplate, { skipFocus: true });
this.titleInput.focus();
} else {
- this.requestFileSuccess(this.currentTemplate, {skipFocus: false});
+ this.requestFileSuccess(this.currentTemplate, { skipFocus: false });
}
return;
}
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
index 0a3890e85fe..97f6d37364d 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-new, comma-dangle, class-methods-use-this, no-param-reassign */
+
((global) => {
class IssuableTemplateSelectors {
constructor({ $dropdowns, editor } = {}) {
@@ -18,7 +19,7 @@
}
initEditor() {
- let editor = $('.markdown-area');
+ const editor = $('.markdown-area');
// Proxy ace-editor's .setValue to jQuery's .val
editor.setValue = editor.val;
editor.getValue = editor.val;
diff --git a/app/assets/javascripts/terminal/terminal.js.es6 b/app/assets/javascripts/terminal/terminal.js.es6
new file mode 100644
index 00000000000..6b9422b1816
--- /dev/null
+++ b/app/assets/javascripts/terminal/terminal.js.es6
@@ -0,0 +1,62 @@
+/* global Terminal */
+
+(() => {
+ class GLTerminal {
+
+ constructor(options) {
+ this.options = options || {};
+
+ this.options.cursorBlink = options.cursorBlink || true;
+ this.options.screenKeys = options.screenKeys || true;
+ this.container = document.querySelector(options.selector);
+
+ this.setSocketUrl();
+ this.createTerminal();
+ $(window).off('resize.terminal').on('resize.terminal', () => {
+ this.terminal.fit();
+ });
+ }
+
+ setSocketUrl() {
+ const { protocol, hostname, port } = window.location;
+ const wsProtocol = protocol === 'https:' ? 'wss://' : 'ws://';
+ const path = this.container.dataset.projectPath;
+
+ this.socketUrl = `${wsProtocol}${hostname}:${port}${path}`;
+ }
+
+ createTerminal() {
+ this.terminal = new Terminal(this.options);
+ this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']);
+ this.socket.binaryType = 'arraybuffer';
+
+ this.terminal.open(this.container);
+ this.socket.onopen = () => { this.runTerminal(); };
+ this.socket.onerror = () => { this.handleSocketFailure(); };
+ }
+
+ runTerminal() {
+ const decoder = new TextDecoder('utf-8');
+ const encoder = new TextEncoder('utf-8');
+
+ this.terminal.on('data', (data) => {
+ this.socket.send(encoder.encode(data));
+ });
+
+ this.socket.addEventListener('message', (ev) => {
+ this.terminal.write(decoder.decode(ev.data));
+ });
+
+ this.isTerminalInitialized = true;
+ this.terminal.fit();
+ }
+
+ handleSocketFailure() {
+ this.terminal.write('\r\nConnection failure');
+ }
+
+ }
+
+ window.gl = window.gl || {};
+ gl.Terminal = GLTerminal;
+})();
diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6
new file mode 100644
index 00000000000..33d2c1e1a17
--- /dev/null
+++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6
@@ -0,0 +1,7 @@
+//= require xterm/encoding-indexes
+//= require xterm/encoding
+//= require xterm/xterm.js
+//= require xterm/fit.js
+//= require ./terminal.js
+
+$(() => new gl.Terminal({ selector: '#terminal' }));
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index 213e80825b7..ef9c0a885fb 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,6 +1,8 @@
-/* eslint-disable */
-((global) => {
+/* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */
+/* global UsersSelect */
+/* global Turbolinks */
+((global) => {
class Todos {
constructor({ el } = {}) {
this.allDoneClicked = this.allDoneClicked.bind(this);
@@ -46,7 +48,7 @@
clicked: function() {
return $dropdown.closest('form.filter-form').submit();
}
- })
+ });
}
doneClicked(e) {
@@ -72,7 +74,7 @@
allDoneClicked(e) {
e.preventDefault();
e.stopImmediatePropagation();
- $target = $(e.currentTarget);
+ const $target = $(e.currentTarget);
$target.disable();
return $.ajax({
type: 'POST',
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index 70aff4b9a2f..d124ca4f88b 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */
+/* global Turbolinks */
(function() {
this.TreeView = (function() {
function TreeView() {
@@ -63,7 +64,5 @@
};
return TreeView;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js.es6
index 35f2b1e2b25..500b78fc5d8 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js.es6
@@ -1,24 +1,33 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
+/* global u2f */
+/* global U2FError */
+/* global U2FUtil */
+
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> authenticated -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ const global = window.gl || (window.gl = {});
+
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
- this.U2FAuthenticate = (function() {
- function U2FAuthenticate(container, u2fParams) {
+ global.U2FAuthenticate = (function() {
+ function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) {
this.container = container;
this.renderNotSupported = bind(this.renderNotSupported, this);
this.renderAuthenticated = bind(this.renderAuthenticated, this);
this.renderError = bind(this.renderError, this);
this.renderInProgress = bind(this.renderInProgress, this);
- this.renderSetup = bind(this.renderSetup, this);
this.renderTemplate = bind(this.renderTemplate, this);
this.authenticate = bind(this.authenticate, this);
this.start = bind(this.start, this);
this.appId = u2fParams.app_id;
this.challenge = u2fParams.challenge;
+ this.form = form;
+ this.fallbackButton = fallbackButton;
+ this.fallbackUI = fallbackUI;
+ if (this.fallbackButton) this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
this.signRequests = u2fParams.sign_requests.map(function(request) {
// The U2F Javascript API v1.1 requires a single challenge, with
// _no challenges per-request_. The U2F Javascript API v1.0 requires a
@@ -37,7 +46,7 @@
U2FAuthenticate.prototype.start = function() {
if (U2FUtil.isU2FSupported()) {
- return this.renderSetup();
+ return this.renderInProgress();
} else {
return this.renderNotSupported();
}
@@ -48,7 +57,7 @@
return function(response) {
var error;
if (response.errorCode) {
- error = new U2FError(response.errorCode);
+ error = new U2FError(response.errorCode, 'authenticate');
return _this.renderError(error);
} else {
return _this.renderAuthenticated(JSON.stringify(response));
@@ -73,11 +82,6 @@
return this.container.html(template(params));
};
- U2FAuthenticate.prototype.renderSetup = function() {
- this.renderTemplate('setup');
- return this.container.find('#js-login-u2f-device').on('click', this.renderInProgress);
- };
-
U2FAuthenticate.prototype.renderInProgress = function() {
this.renderTemplate('inProgress');
return this.authenticate();
@@ -85,24 +89,30 @@
U2FAuthenticate.prototype.renderError = function(error) {
this.renderTemplate('error', {
- error_message: error.message()
+ error_message: error.message(),
+ error_code: error.errorCode
});
- return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
+ return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
};
U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
this.renderTemplate('authenticated');
- // Prefer to do this instead of interpolating using Underscore templates
- // because of JSON escaping issues.
- return this.container.find("#js-device-response").val(deviceResponse);
+ const container = this.container[0];
+ container.querySelector('#js-device-response').value = deviceResponse;
+ container.querySelector(this.form).submit();
+ this.fallbackButton.classList.add('hidden');
};
U2FAuthenticate.prototype.renderNotSupported = function() {
return this.renderTemplate('notSupported');
};
- return U2FAuthenticate;
+ U2FAuthenticate.prototype.switchToFallbackUI = function() {
+ this.fallbackButton.classList.add('hidden');
+ this.container[0].classList.add('hidden');
+ this.fallbackUI.classList.remove('hidden');
+ };
+ return U2FAuthenticate;
})();
-
-}).call(this);
+})();
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index aff605169e4..86b459e1866 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,28 +1,27 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, max-len */
+/* global u2f */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.U2FError = (function() {
- function U2FError(errorCode) {
+ function U2FError(errorCode, u2fFlowType) {
this.errorCode = errorCode;
this.message = bind(this.message, this);
this.httpsDisabled = window.location.protocol !== 'https:';
- console.error("U2F Error Code: " + this.errorCode);
+ this.u2fFlowType = u2fFlowType;
}
U2FError.prototype.message = function() {
- switch (false) {
- case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled):
- return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details.";
- case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE:
- return "This device has already been registered with us.";
- default:
- return "There was a problem communicating with your device.";
+ if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
+ return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
+ } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) {
+ if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.';
+ if (this.u2fFlowType === 'register') return 'This device has already been registered with us.';
}
+ return "There was a problem communicating with your device.";
};
return U2FError;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 22fbf9f3a91..69d1ff3a39e 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,10 +1,14 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
+/* global u2f */
+/* global U2FError */
+/* global U2FUtil */
+
// Register U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> registered -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.U2FRegister = (function() {
function U2FRegister(container, u2fParams) {
@@ -35,7 +39,7 @@
return function(response) {
var error;
if (response.errorCode) {
- error = new U2FError(response.errorCode);
+ error = new U2FError(response.errorCode, 'register');
return _this.renderError(error);
} else {
return _this.renderRegistered(JSON.stringify(response));
@@ -72,7 +76,8 @@
U2FRegister.prototype.renderError = function(error) {
this.renderTemplate('error', {
- error_message: error.message()
+ error_message: error.message(),
+ error_code: error.errorCode
});
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
};
@@ -89,7 +94,5 @@
};
return U2FRegister;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 2eab2d5ae23..34e88220b12 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife */
(function() {
this.U2FUtil = (function() {
function U2FUtil() {}
@@ -8,7 +8,5 @@
};
return U2FUtil;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
index 5e869e99fdb..059e6c628b3 100644
--- a/app/assets/javascripts/user.js.es6
+++ b/app/assets/javascripts/user.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */
+/* global Cookies */
+
((global) => {
global.User = class {
constructor({ action }) {
@@ -28,5 +30,5 @@
$(this).parents('.project-limit-message').remove();
});
}
- }
+ };
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
index 2b310da319c..313fb17aee8 100644
--- a/app/assets/javascripts/user_tabs.js.es6
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign */
+
/*
UserTabs
@@ -106,7 +107,7 @@ content on the Users#show page.
this.loadActivities(source);
}
- const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ];
+ const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
return this.loadTab(source, action);
}
@@ -134,7 +135,7 @@ content on the Users#show page.
}
const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
- new Activities();
+ new gl.Activities();
return this.loaded['activity'] = true;
}
@@ -144,7 +145,7 @@ content on the Users#show page.
}
setCurrentAction(source, action) {
- let new_state = source
+ let new_state = source;
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
history.replaceState({
diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6
index c4dde575c6e..137cefa3b8e 100644
--- a/app/assets/javascripts/username_validator.js.es6
+++ b/app/assets/javascripts/username_validator.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
+
((global) => {
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
@@ -77,7 +78,7 @@
this.renderState();
return $.ajax({
type: 'GET',
- url: `/users/${username}/exists`,
+ url: `${gon.relative_url_root}/users/${username}/exists`,
dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists)
});
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 0ec878e7e60..7ffc546ffc1 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,6 +1,9 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */
+/* global d3 */
+/* global dateFormat */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Calendar = (function() {
function Calendar(timestamps, calendar_activities_path) {
@@ -17,7 +20,7 @@
this.timestampsTmp = [];
var group = 0;
- var today = new Date()
+ var today = new Date();
today.setHours(0, 0, 0, 0, 0);
var oneYearAgo = new Date(today);
@@ -25,7 +28,7 @@
var days = gl.utils.getDayDifference(oneYearAgo, today);
- for(var i = 0; i <= days; i++) {
+ for (var i = 0; i <= days; i += 1) {
var date = new Date(oneYearAgo);
date.setDate(date.getDate() + i);
@@ -36,7 +39,7 @@
// or if is first object
if ((day === 0 && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
- group++;
+ group += 1;
}
var innerArray = this.timestampsTmp[group - 1];
@@ -71,7 +74,7 @@
}
return extraWidthPadding;
- }
+ };
Calendar.prototype.renderSvg = function(group) {
var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
@@ -206,6 +209,7 @@
}
});
} else {
+ this.currentSelectedDate = '';
return $('.user-calendar-activities').html('');
}
};
@@ -217,7 +221,5 @@
};
return Calendar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index 22bee0f6187..f50802bdf2e 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require_tree . */
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 7a2221dbaf5..77d2764cdf0 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,6 +1,10 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
+/* global Vue */
+/* global Issuable */
+/* global ListUser */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
slice = [].slice;
this.UsersSelect = (function() {
@@ -112,7 +116,7 @@
showDivider = 0;
if (firstUser) {
// Move current user to the front of the list
- for (index = j = 0, len = users.length; j < len; index = ++j) {
+ for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
obj = users[index];
if (obj.username === firstUser) {
users.splice(index, 1);
@@ -274,7 +278,7 @@
if (firstUser) {
// Move current user to the front of the list
ref = data.results;
- for (index = j = 0, len = ref.length; j < len; index = ++j) {
+ for (index = j = 0, len = ref.length; j < len; index = (j += 1)) {
obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
@@ -367,7 +371,7 @@
};
UsersSelect.prototype.user = function(user_id, callback) {
- if(!/^\d+$/.test(user_id)) {
+ if (!/^\d+$/.test(user_id)) {
return false;
}
@@ -417,7 +421,5 @@
};
return UsersSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/visibility_select.js.es6 b/app/assets/javascripts/visibility_select.js.es6
new file mode 100644
index 00000000000..f712d7ba930
--- /dev/null
+++ b/app/assets/javascripts/visibility_select.js.es6
@@ -0,0 +1,27 @@
+(() => {
+ const gl = window.gl || (window.gl = {});
+
+ class VisibilitySelect {
+ constructor(container) {
+ if (!container) throw new Error('VisibilitySelect requires a container element as argument 1');
+ this.container = container;
+ this.helpBlock = this.container.querySelector('.help-block');
+ this.select = this.container.querySelector('select');
+ }
+
+ init() {
+ if (this.select) {
+ this.updateHelpText();
+ this.select.addEventListener('change', this.updateHelpText.bind(this));
+ } else {
+ this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock;
+ }
+ }
+
+ updateHelpText() {
+ this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description;
+ }
+ }
+
+ gl.VisibilitySelect = VisibilitySelect;
+})();
diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6
new file mode 100644
index 00000000000..62a22e39a3b
--- /dev/null
+++ b/app/assets/javascripts/vue_common_component/commit.js.es6
@@ -0,0 +1,163 @@
+/*= require vue */
+/* global Vue */
+(() => {
+ window.gl = window.gl || {};
+
+ window.gl.CommitComponent = Vue.component('commit-component', {
+
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render `fa-code-fork` icon.
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+
+ commitIconSvg: {
+ type: String,
+ required: false,
+ },
+ },
+
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+ },
+
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author &&
+ this.author.avatar_url &&
+ this.author.web_url &&
+ this.author.username;
+ },
+
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author &&
+ this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+
+ template: `
+ <div class="branch-commit">
+
+ <div v-if="hasCommitRef" class="icon-container">
+ <i v-if="tag" class="fa fa-tag"></i>
+ <i v-if="!tag" class="fa fa-code-fork"></i>
+ </div>
+
+ <a v-if="hasCommitRef"
+ class="monospace branch-name"
+ :href="commitRef.ref_url">
+ {{commitRef.name}}
+ </a>
+
+ <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+ <a class="commit-id monospace"
+ :href="commitUrl">
+ {{shortSha}}
+ </a>
+
+ <p class="commit-title">
+ <span v-if="title">
+ <a v-if="hasAuthor"
+ class="avatar-image-container"
+ :href="author.web_url">
+ <img
+ class="avatar has-tooltip s20"
+ :src="author.avatar_url"
+ :alt="userImageAltDescription"
+ :title="author.username" />
+ </a>
+
+ <a class="commit-row-message"
+ :href="commitUrl">
+ {{title}}
+ </a>
+ </span>
+ <span v-else>
+ Cant find HEAD commit for this branch
+ </span>
+ </p>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_pagination/index.js.es6
new file mode 100644
index 00000000000..605824fa939
--- /dev/null
+++ b/app/assets/javascripts/vue_pagination/index.js.es6
@@ -0,0 +1,148 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign, no-plusplus */
+
+((gl) => {
+ const PAGINATION_UI_BUTTON_LIMIT = 4;
+ const UI_LIMIT = 6;
+ const SPREAD = '...';
+ const PREV = 'Prev';
+ const NEXT = 'Next';
+ const FIRST = '<< First';
+ const LAST = 'Last >>';
+
+ gl.VueGlPagination = Vue.extend({
+ props: {
+
+ /**
+ This function will take the information given by the pagination component
+ And make a new Turbolinks call
+
+ Here is an example `change` method:
+
+ change(pagenum, apiScope) {
+ Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
+ },
+ */
+
+ change: {
+ type: Function,
+ required: true,
+ },
+
+ /**
+ pageInfo will come from the headers of the API call
+ in the `.then` clause of the VueResource API call
+ there should be a function that contructs the pageInfo for this component
+
+ This is an example:
+
+ const pageInfo = headers => ({
+ perPage: +headers['X-Per-Page'],
+ page: +headers['X-Page'],
+ total: +headers['X-Total'],
+ totalPages: +headers['X-Total-Pages'],
+ nextPage: +headers['X-Next-Page'],
+ previousPage: +headers['X-Prev-Page'],
+ });
+ */
+
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ changePage(e) {
+ let apiScope = gl.utils.getParameterByName('scope');
+
+ if (!apiScope) apiScope = 'all';
+
+ const text = e.target.innerText;
+ const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+ switch (text) {
+ case SPREAD:
+ break;
+ case LAST:
+ this.change(totalPages, apiScope);
+ break;
+ case NEXT:
+ this.change(nextPage, apiScope);
+ break;
+ case PREV:
+ this.change(previousPage, apiScope);
+ break;
+ case FIRST:
+ this.change(1, apiScope);
+ break;
+ default:
+ this.change(+text, apiScope);
+ break;
+ }
+ },
+ },
+ computed: {
+ prev() {
+ return this.pageInfo.previousPage;
+ },
+ next() {
+ return this.pageInfo.nextPage;
+ },
+ getItems() {
+ const total = this.pageInfo.totalPages;
+ const page = this.pageInfo.page;
+ const items = [];
+
+ if (page > 1) items.push({ title: FIRST });
+
+ if (page > 1) {
+ items.push({ title: PREV, prev: true });
+ } else {
+ items.push({ title: PREV, disabled: true, prev: true });
+ }
+
+ if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+
+ const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+ const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+
+ for (let i = start; i <= end; i++) {
+ const isActive = i === page;
+ items.push({ title: i, active: isActive, page: true });
+ }
+
+ if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+ items.push({ title: SPREAD, separator: true, page: true });
+ }
+
+ if (page === total) {
+ items.push({ title: NEXT, disabled: true, next: true });
+ } else if (total - page >= 1) {
+ items.push({ title: NEXT, next: true });
+ }
+
+ if (total - page >= 1) items.push({ title: LAST, last: true });
+
+ return items;
+ },
+ },
+ template: `
+ <div class="gl-pagination">
+ <ul class="pagination clearfix">
+ <li v-for='item in getItems'
+ :class='{
+ page: item.page,
+ prev: item.prev,
+ next: item.next,
+ separator: item.separator,
+ active: item.active,
+ disabled: item.disabled
+ }'
+ >
+ <a @click="changePage($event)">{{item.title}}</a>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6
new file mode 100644
index 00000000000..edd01f17a97
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6
@@ -0,0 +1,42 @@
+/* global Vue, VueResource, gl */
+/*= require vue_common_component/commit */
+/*= require vue_pagination/index */
+/*= require vue-resource
+/*= require boards/vue_resource_interceptor */
+/*= require ./status.js.es6 */
+/*= require ./store.js.es6 */
+/*= require ./pipeline_url.js.es6 */
+/*= require ./stage.js.es6 */
+/*= require ./stages.js.es6 */
+/*= require ./pipeline_actions.js.es6 */
+/*= require ./time_ago.js.es6 */
+/*= require ./pipelines.js.es6 */
+
+(() => {
+ const project = document.querySelector('.pipelines');
+ const entry = document.querySelector('.vue-pipelines-index');
+ const svgs = document.querySelector('.pipeline-svgs');
+
+ Vue.use(VueResource);
+
+ if (!entry) return null;
+ return new Vue({
+ el: entry,
+ data: {
+ scope: project.dataset.url,
+ store: new gl.PipelineStore(),
+ svgs: svgs.dataset,
+ },
+ components: {
+ 'vue-pipelines': gl.VuePipelines,
+ },
+ template: `
+ <vue-pipelines
+ :scope='scope'
+ :store='store'
+ :svgs='svgs'
+ >
+ </vue-pipelines>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
new file mode 100644
index 00000000000..ad5cb30cc42
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
@@ -0,0 +1,99 @@
+/* global Vue, Flash, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VuePipelineActions = Vue.extend({
+ props: ['pipeline', 'svgs'],
+ computed: {
+ actions() {
+ return this.pipeline.details.manual_actions.length > 0;
+ },
+ artifacts() {
+ return this.pipeline.details.artifacts.length > 0;
+ },
+ },
+ methods: {
+ download(name) {
+ return `Download ${name} artifacts`;
+ },
+ },
+ template: `
+ <td class="pipeline-actions hidden-xs">
+ <div class="controls pull-right">
+ <div class="btn-group inline">
+ <div class="btn-group">
+ <a
+ v-if='actions'
+ class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
+ data-toggle="dropdown"
+ title="Manual build"
+ alt="Manual Build"
+ >
+ <span v-html='svgs.iconPlay'></span>
+ <i class="fa fa-caret-down"></i>
+ </a>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for='action in pipeline.details.manual_actions'>
+ <a
+ rel="nofollow"
+ data-method="post"
+ :href='action.path'
+ title="Manual build"
+ >
+ <span v-html='svgs.iconPlay'></span>
+ <span title="Manual build">{{action.name}}</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ <div class="btn-group">
+ <a
+ v-if='artifacts'
+ class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
+ data-toggle="dropdown"
+ type="button"
+ >
+ <i class="fa fa-download"></i>
+ <i class="fa fa-caret-down"></i>
+ </a>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for='artifact in pipeline.details.artifacts'>
+ <a
+ rel="nofollow"
+ :href='artifact.path'
+ >
+ <i class="fa fa-download"></i>
+ <span>{{download(artifact.name)}}</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="cancel-retry-btns inline">
+ <a
+ v-if='pipeline.flags.retryable'
+ class="btn has-tooltip"
+ title="Retry"
+ rel="nofollow"
+ data-method="post"
+ :href='pipeline.retry_path'
+ >
+ <i class="fa fa-repeat"></i>
+ </a>
+ <a
+ v-if='pipeline.flags.cancelable'
+ class="btn btn-remove has-tooltip"
+ title="Cancel"
+ rel="nofollow"
+ data-method="post"
+ :href='pipeline.cancel_path'
+ data-original-title="Cancel"
+ >
+ <i class="fa fa-remove"></i>
+ </a>
+ </div>
+ </div>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
new file mode 100644
index 00000000000..ae5649f0519
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
@@ -0,0 +1,63 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VuePipelineUrl = Vue.extend({
+ props: [
+ 'pipeline',
+ ],
+ computed: {
+ user() {
+ return !!this.pipeline.user;
+ },
+ },
+ template: `
+ <td>
+ <a :href='pipeline.path'>
+ <span class="pipeline-id">#{{pipeline.id}}</span>
+ </a>
+ <span>by</span>
+ <a
+ v-if='user'
+ :href='pipeline.user.web_url'
+ >
+ <img
+ v-if='user'
+ class="avatar has-tooltip s20 "
+ :title='pipeline.user.name'
+ data-container="body"
+ :src='pipeline.user.avatar_url'
+ >
+ </a>
+ <span
+ v-if='!user'
+ class="api monospace"
+ >
+ API
+ </span>
+ <span
+ v-if='pipeline.flags.latest'
+ class="label label-success has-tooltip"
+ title="Latest pipeline for this branch"
+ data-original-title="Latest pipeline for this branch"
+ >
+ latest
+ </span>
+ <span
+ v-if='pipeline.flags.yaml_errors'
+ class="label label-danger has-tooltip"
+ :title='pipeline.yaml_errors'
+ :data-original-title='pipeline.yaml_errors'
+ >
+ yaml invalid
+ </span>
+ <span
+ v-if='pipeline.flags.stuck'
+ class="label label-warning"
+ >
+ stuck
+ </span>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
new file mode 100644
index 00000000000..b2ed05503c9
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
@@ -0,0 +1,131 @@
+/* global Vue, Turbolinks, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VuePipelines = Vue.extend({
+ components: {
+ runningPipeline: gl.VueRunningPipeline,
+ pipelineActions: gl.VuePipelineActions,
+ stages: gl.VueStages,
+ commit: gl.CommitComponent,
+ pipelineUrl: gl.VuePipelineUrl,
+ pipelineHead: gl.VuePipelineHead,
+ glPagination: gl.VueGlPagination,
+ statusScope: gl.VueStatusScope,
+ timeAgo: gl.VueTimeAgo,
+ },
+ data() {
+ return {
+ pipelines: [],
+ timeLoopInterval: '',
+ intervalId: '',
+ apiScope: 'all',
+ pageInfo: {},
+ pagenum: 1,
+ count: { all: 0, running_or_pending: 0 },
+ pageRequest: false,
+ };
+ },
+ props: ['scope', 'store', 'svgs'],
+ created() {
+ const pagenum = gl.utils.getParameterByName('p');
+ const scope = gl.utils.getParameterByName('scope');
+ if (pagenum) this.pagenum = pagenum;
+ if (scope) this.apiScope = scope;
+ this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
+ },
+ methods: {
+ change(pagenum, apiScope) {
+ Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
+ },
+ author(pipeline) {
+ if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' };
+ if (pipeline.commit.author) return pipeline.commit.author;
+ return {
+ avatar_url: pipeline.commit.author_gravatar_url,
+ web_url: `mailto:${pipeline.commit.author_email}`,
+ username: pipeline.commit.author_name,
+ };
+ },
+ ref(pipeline) {
+ const { ref } = pipeline;
+ return { name: ref.name, tag: ref.tag, ref_url: ref.path };
+ },
+ commitTitle(pipeline) {
+ return pipeline.commit ? pipeline.commit.title : '';
+ },
+ commitSha(pipeline) {
+ return pipeline.commit ? pipeline.commit.short_id : '';
+ },
+ commitUrl(pipeline) {
+ return pipeline.commit ? pipeline.commit.commit_path : '';
+ },
+ match(string) {
+ return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
+ },
+ },
+ template: `
+ <div>
+ <div class="pipelines realtime-loading" v-if='pipelines.length < 1'>
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+ <div class="table-holder" v-if='pipelines.length'>
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="pipeline-status">Status</th>
+ <th class="pipeline-info">Pipeline</th>
+ <th class="pipeline-commit">Commit</th>
+ <th class="pipeline-stages">Stages</th>
+ <th class="pipeline-date"></th>
+ <th class="pipeline-actions hidden-xs"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr class="commit" v-for='pipeline in pipelines'>
+ <status-scope
+ :pipeline='pipeline'
+ :match='match'
+ :svgs='svgs'
+ >
+ </status-scope>
+ <pipeline-url :pipeline='pipeline'></pipeline-url>
+ <td>
+ <commit
+ :commit-icon-svg='svgs.commitIconSvg'
+ :author='author(pipeline)'
+ :tag="pipeline.ref.tag"
+ :title='commitTitle(pipeline)'
+ :commit-ref='ref(pipeline)'
+ :short-sha='commitSha(pipeline)'
+ :commit-url='commitUrl(pipeline)'
+ >
+ </commit>
+ </td>
+ <stages
+ :pipeline='pipeline'
+ :svgs='svgs'
+ :match='match'
+ >
+ </stages>
+ <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago>
+ <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="pipelines realtime-loading" v-if='pageRequest'>
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+ <gl-pagination
+ v-if='pageInfo.total > pageInfo.perPage'
+ :pagenum='pagenum'
+ :change='change'
+ :count='count.all'
+ :pageInfo='pageInfo'
+ >
+ </gl-pagination>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
new file mode 100644
index 00000000000..4e85f16ebc5
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
@@ -0,0 +1,102 @@
+/* global Vue, Flash, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueStage = Vue.extend({
+ data() {
+ return {
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ };
+ },
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ svgs: {
+ type: DOMStringMap,
+ required: true,
+ },
+ match: {
+ type: Function,
+ required: true,
+ },
+ },
+ methods: {
+ fetchBuilds(e) {
+ const areaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+ if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
+
+ return this.$http.get(this.stage.dropdown_path)
+ .then((response) => {
+ this.builds = JSON.parse(response.body).html;
+ }, () => {
+ const flash = new Flash('Something went wrong on our end.');
+ return flash;
+ });
+ },
+ keepGraph(e) {
+ const { target } = e;
+
+ if (target.className.indexOf('js-ci-action-icon') >= 0) return null;
+
+ if (
+ target.parentElement &&
+ (target.parentElement.className.indexOf('js-ci-action-icon') >= 0)
+ ) return null;
+
+ return e.stopPropagation();
+ },
+ },
+ computed: {
+ buildsOrSpinner() {
+ return this.builds ? this.builds : this.spinner;
+ },
+ dropdownClass() {
+ if (this.builds) return 'js-builds-dropdown-container';
+ return 'js-builds-dropdown-loading builds-dropdown-loading';
+ },
+ buildStatus() {
+ return `Build: ${this.stage.status.label}`;
+ },
+ tooltip() {
+ return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+ },
+ svg() {
+ const { icon } = this.stage.status;
+ const stageIcon = icon.replace(/icon/i, 'stage_icon');
+ return this.svgs[this.match(stageIcon)];
+ },
+ triggerButtonClass() {
+ return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+ },
+ },
+ template: `
+ <div>
+ <button
+ @click='fetchBuilds($event)'
+ :class="triggerButtonClass"
+ :title='stage.title'
+ data-placement="top"
+ data-toggle="dropdown"
+ type="button"
+ >
+ <span v-html="svg"></span>
+ <i class="fa fa-caret-down "></i>
+ </button>
+ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <div class="arrow-up"></div>
+ <div
+ @click='keepGraph($event)'
+ :class="dropdownClass"
+ class="js-builds-dropdown-list scrollable-menu"
+ v-html="buildsOrSpinner"
+ >
+ </div>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6
new file mode 100644
index 00000000000..cb176b3f0c6
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/stages.js.es6
@@ -0,0 +1,21 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueStages = Vue.extend({
+ components: {
+ 'vue-stage': gl.VueStage,
+ },
+ props: ['pipeline', 'svgs', 'match'],
+ template: `
+ <td class="stage-cell">
+ <div
+ class="stage-container dropdown js-mini-pipeline-graph"
+ v-for='stage in pipeline.details.stages'
+ >
+ <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage>
+ </div>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6
new file mode 100644
index 00000000000..05175082704
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/status.js.es6
@@ -0,0 +1,34 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueStatusScope = Vue.extend({
+ props: [
+ 'pipeline', 'svgs', 'match',
+ ],
+ computed: {
+ cssClasses() {
+ const cssObject = { 'ci-status': true };
+ cssObject[`ci-${this.pipeline.details.status.group}`] = true;
+ return cssObject;
+ },
+ svg() {
+ return this.svgs[this.match(this.pipeline.details.status.icon)];
+ },
+ detailsPath() {
+ const { status } = this.pipeline.details;
+ return status.has_details ? status.details_path : false;
+ },
+ },
+ template: `
+ <td class="commit-link">
+ <a
+ :class='cssClasses'
+ :href='detailsPath'
+ v-html='svg + pipeline.details.status.text'
+ >
+ </a>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
new file mode 100644
index 00000000000..1982142853a
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -0,0 +1,69 @@
+/* global gl, Flash */
+/* eslint-disable no-param-reassign, no-underscore-dangle */
+/*= require vue_realtime_listener/index.js */
+
+((gl) => {
+ const pageValues = (headers) => {
+ const normalizedHeaders = {};
+
+ Object.keys(headers).forEach((e) => {
+ normalizedHeaders[e.toUpperCase()] = headers[e];
+ });
+
+ const paginationInfo = {
+ perPage: +normalizedHeaders['X-PER-PAGE'],
+ page: +normalizedHeaders['X-PAGE'],
+ total: +normalizedHeaders['X-TOTAL'],
+ totalPages: +normalizedHeaders['X-TOTAL-PAGES'],
+ nextPage: +normalizedHeaders['X-NEXT-PAGE'],
+ previousPage: +normalizedHeaders['X-PREV-PAGE'],
+ };
+
+ return paginationInfo;
+ };
+
+ gl.PipelineStore = class {
+ fetchDataLoop(Vue, pageNum, url, apiScope) {
+ const updatePipelineNums = (count) => {
+ const { all } = count;
+ const running = count.running_or_pending;
+ document.querySelector('.js-totalbuilds-count').innerHTML = all;
+ document.querySelector('.js-running-count').innerHTML = running;
+ };
+
+ const goFetch = () =>
+ this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
+ .then((response) => {
+ const pageInfo = pageValues(response.headers);
+ this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
+
+ const res = JSON.parse(response.body);
+ this.count = Object.assign({}, this.count, res.count);
+ this.pipelines = Object.assign([], this.pipelines, res.pipelines);
+
+ updatePipelineNums(this.count);
+ this.pageRequest = false;
+ }, () => {
+ this.pageRequest = false;
+ return new Flash('Something went wrong on our end.');
+ });
+
+ goFetch();
+
+ const startTimeLoops = () => {
+ this.timeLoopInterval = setInterval(() => {
+ this.$children
+ .filter(e => e.$options._componentTag === 'time-ago')
+ .forEach(e => e.changeTime());
+ }, 10000);
+ };
+
+ startTimeLoops();
+
+ const removeIntervals = () => clearInterval(this.timeLoopInterval);
+ const startIntervals = () => startTimeLoops();
+
+ gl.VueRealtimeListener(removeIntervals, startIntervals);
+ }
+ };
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
new file mode 100644
index 00000000000..655110feba1
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
@@ -0,0 +1,73 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueTimeAgo = Vue.extend({
+ data() {
+ return {
+ currentTime: new Date(),
+ };
+ },
+ props: ['pipeline', 'svgs'],
+ computed: {
+ timeAgo() {
+ return gl.utils.getTimeago();
+ },
+ localTimeFinished() {
+ return gl.utils.formatDate(this.pipeline.details.finished_at);
+ },
+ timeStopped() {
+ const changeTime = this.currentTime;
+ const options = {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ };
+ options.timeZoneName = 'short';
+ const finished = this.pipeline.details.finished_at;
+ if (!finished && changeTime) return false;
+ return ({ words: this.timeAgo.format(finished) });
+ },
+ duration() {
+ const { duration } = this.pipeline.details;
+ const date = new Date(duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ if (hh < 10) hh = `0${hh}`;
+ if (mm < 10) mm = `0${mm}`;
+ if (ss < 10) ss = `0${ss}`;
+
+ if (duration !== null) return `${hh}:${mm}:${ss}`;
+ return false;
+ },
+ },
+ methods: {
+ changeTime() {
+ this.currentTime = new Date();
+ },
+ },
+ template: `
+ <td>
+ <p class="duration" v-if='duration'>
+ <span v-html='svgs.iconTimer'></span>
+ {{duration}}
+ </p>
+ <p class="finished-at" v-if='timeStopped'>
+ <i class="fa fa-calendar"></i>
+ <time
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ :data-original-title='localTimeFinished'
+ >
+ {{timeStopped.words}}
+ </time>
+ </p>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6
new file mode 100644
index 00000000000..23cac1466d2
--- /dev/null
+++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6
@@ -0,0 +1,18 @@
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
+ const removeAll = () => {
+ removeIntervals();
+ window.removeEventListener('beforeunload', removeIntervals);
+ window.removeEventListener('focus', startIntervals);
+ window.removeEventListener('blur', removeIntervals);
+ document.removeEventListener('page:fetch', removeAll);
+ };
+
+ window.addEventListener('beforeunload', removeIntervals);
+ window.addEventListener('focus', startIntervals);
+ window.addEventListener('blur', removeIntervals);
+ document.addEventListener('page:fetch', removeAll);
+ };
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
deleted file mode 100644
index ad9b842db3c..00000000000
--- a/app/assets/javascripts/wikis.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* eslint-disable */
-
-/*= require latinise */
-
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.Wikis = (function() {
- function Wikis() {
- this.slugify = bind(this.slugify, this);
- $('.new-wiki-page').on('submit', (function(_this) {
- return function(e) {
- var field, path, slug;
- $('[data-error~=slug]').addClass('hidden');
- field = $('#new_wiki_path');
- slug = _this.slugify(field.val());
- if (slug.length > 0) {
- path = field.attr('data-wikis-path');
- location.href = path + '/' + slug;
- return e.preventDefault();
- }
- };
- })(this));
- }
-
- Wikis.prototype.dasherize = function(value) {
- return value.replace(/[_\s]+/g, '-');
- };
-
- Wikis.prototype.slugify = function(value) {
- return this.dasherize(value.trim().toLowerCase().latinise());
- };
-
- return Wikis;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6
new file mode 100644
index 00000000000..ecff5fd5bf4
--- /dev/null
+++ b/app/assets/javascripts/wikis.js.es6
@@ -0,0 +1,73 @@
+/* eslint-disable no-param-reassign */
+/* global Breakpoints */
+
+/*= require latinise */
+/*= require breakpoints */
+/*= require jquery.nicescroll */
+
+((global) => {
+ const dasherize = str => str.replace(/[_\s]+/g, '-');
+ const slugify = str => dasherize(str.trim().toLowerCase().latinise());
+
+ class Wikis {
+ constructor() {
+ this.bp = Breakpoints.get();
+ this.sidebarEl = document.querySelector('.js-wiki-sidebar');
+ this.sidebarExpanded = false;
+ $(this.sidebarEl).niceScroll();
+
+ const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle');
+ for (let i = 0; i < sidebarToggles.length; i += 1) {
+ sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
+ }
+
+ this.newWikiForm = document.querySelector('form.new-wiki-page');
+ if (this.newWikiForm) {
+ this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
+ }
+
+ window.addEventListener('resize', () => this.renderSidebar());
+ this.renderSidebar();
+ }
+
+ handleNewWikiSubmit(e) {
+ if (!this.newWikiForm) return;
+
+ const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
+ const slug = slugify(slugInput.value);
+
+ if (slug.length > 0) {
+ const wikisPath = slugInput.getAttribute('data-wikis-path');
+ window.location.href = `${wikisPath}/${slug}`;
+ e.preventDefault();
+ }
+ }
+
+ handleToggleSidebar(e) {
+ e.preventDefault();
+ this.sidebarExpanded = !this.sidebarExpanded;
+ this.renderSidebar();
+ }
+
+ sidebarCanCollapse() {
+ const bootstrapBreakpoint = this.bp.getBreakpointSize();
+ return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ }
+
+ renderSidebar() {
+ if (!this.sidebarEl) return;
+ const { classList } = this.sidebarEl;
+ if (this.sidebarExpanded || !this.sidebarCanCollapse()) {
+ if (!classList.contains('right-sidebar-expanded')) {
+ classList.remove('right-sidebar-collapsed');
+ classList.add('right-sidebar-expanded');
+ }
+ } else if (classList.contains('right-sidebar-expanded')) {
+ classList.add('right-sidebar-collapsed');
+ classList.remove('right-sidebar-expanded');
+ }
+ }
+ }
+
+ global.Wikis = Wikis;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index fa124e7052d..a8b7be7ad06 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len */
+/* global Dropzone */
+/* global Mousetrap */
+
// Zen Mode (full screen) textarea
//
/*= provides zen_mode:enter */
@@ -90,7 +93,5 @@
};
return ZenMode;
-
})();
-
}).call(this);
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index d5cca1b10fb..3cf49f4ff1b 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -1,4 +1,3 @@
-@import "framework/fonts";
@import "framework/variables";
@import "framework/mixins";
@import 'framework/tw_bootstrap_variables';
@@ -7,8 +6,10 @@
@import "framework/animations.scss";
@import "framework/avatar.scss";
+@import "framework/asciidoctor.scss";
@import "framework/blocks.scss";
@import "framework/buttons.scss";
+@import "framework/badges.scss";
@import "framework/calendar.scss";
@import "framework/callout.scss";
@import "framework/common.scss";
@@ -39,3 +40,11 @@
@import "framework/typography.scss";
@import "framework/zen.scss";
@import "framework/blank";
+@import "framework/wells.scss";
+@import "framework/page-header.scss";
+@import "framework/awards.scss";
+@import "framework/images.scss";
+@import "framework/broadcast-messages";
+@import "framework/emojis.scss";
+@import "framework/icons.scss";
+@import "framework/snippets.scss";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index f1d36efb3de..8d38fc78a19 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -50,3 +50,77 @@
.pulse {
@include webkit-prefix(animation-name, pulse);
}
+
+/*
+* General hover animations
+*/
+
+
+// Sass multiple transitions mixin | https://gist.github.com/tobiasahlin/7a421fb9306a4f518aab
+// Usage: @include transition(width, height 0.3s ease-in-out);
+// Output: -webkit-transition(width 0.2s, height 0.3s ease-in-out);
+// transition(width 0.2s, height 0.3s ease-in-out);
+//
+// Pass in any number of transitions
+@mixin transition($transitions...) {
+ $unfoldedTransitions: ();
+ @each $transition in $transitions {
+ $unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma);
+ }
+
+ transition: $unfoldedTransitions;
+}
+
+@function unfoldTransition ($transition) {
+ // Default values
+ $property: all;
+ $duration: $general-hover-transition-duration;
+ $easing: $general-hover-transition-curve; // Browser default is ease, which is what we want
+ $delay: null; // Browser default is 0, which is what we want
+ $defaultProperties: ($property, $duration, $easing, $delay);
+
+ // Grab transition properties if they exist
+ $unfoldedTransition: ();
+ @for $i from 1 through length($defaultProperties) {
+ $p: null;
+ @if $i <= length($transition) {
+ $p: nth($transition, $i);
+ } @else {
+ $p: nth($defaultProperties, $i);
+ }
+ $unfoldedTransition: append($unfoldedTransition, $p);
+ }
+
+ @return $unfoldedTransition;
+}
+
+.btn,
+.side-nav-toggle {
+ @include transition(background-color, border-color, color, box-shadow);
+}
+
+.dropdown-menu-toggle,
+.avatar-circle,
+.header-user-avatar {
+ @include transition(border-color);
+}
+
+.note-action-button .link-highlight,
+.toolbar-btn,
+.dropdown-toggle-caret,
+.fa:not(.fa-bell) {
+ @include transition(color);
+}
+
+a {
+ @include transition(background-color, color, border);
+}
+
+.tree-table td,
+.well-list > li {
+ @include transition(background-color, border-color);
+}
+
+.stage-nav-item {
+ @include transition(background-color, box-shadow);
+}
diff --git a/app/assets/stylesheets/framework/asciidoctor.scss b/app/assets/stylesheets/framework/asciidoctor.scss
new file mode 100644
index 00000000000..62493c32833
--- /dev/null
+++ b/app/assets/stylesheets/framework/asciidoctor.scss
@@ -0,0 +1,27 @@
+.admonitionblock td.icon {
+ width: 1%;
+
+ [class^="fa icon-"] {
+ @extend .fa-2x;
+ }
+
+ .icon-note {
+ @extend .fa-thumb-tack;
+ }
+
+ .icon-tip {
+ @extend .fa-lightbulb-o;
+ }
+
+ .icon-warning {
+ @extend .fa-exclamation-triangle;
+ }
+
+ .icon-caution {
+ @extend .fa-fire;
+ }
+
+ .icon-important {
+ @extend .fa-exclamation-circle;
+ }
+}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 202ed5ae8fe..8392b98f0a7 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -8,7 +8,7 @@
float: left;
margin-right: 15px;
border-radius: $avatar_radius;
- border: 1px solid rgba(0, 0, 0, .1);
+ border: 1px solid $avatar-border;
&.s16 { @include avatar-size(16px, 6px); }
&.s20 { @include avatar-size(20px, 7px); }
&.s24 { @include avatar-size(24px, 8px); }
@@ -34,6 +34,7 @@
&.avatar-inline {
float: none;
+ display: inline-block;
margin-left: 4px;
margin-bottom: 2px;
@@ -41,10 +42,20 @@
&.s24 { margin-right: 4px; }
}
+ &.center {
+ font-size: 14px;
+ line-height: 1.8em;
+ text-align: center;
+ }
+
&.avatar-tile {
border-radius: 0;
border: none;
}
+
+ &:not([href]):hover {
+ border-color: rgba($avatar-border, .2);
+ }
}
.identicon {
@@ -57,7 +68,7 @@
&.s32 { font-size: 20px; line-height: 30px; }
&.s40 { font-size: 16px; line-height: 38px; }
&.s60 { font-size: 32px; line-height: 58px; }
- &.s70 { font-size: 34px; line-height: 68px; }
+ &.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
&.s110 { font-size: 40px; line-height: 108px; font-weight: 300; }
&.s140 { font-size: 72px; line-height: 138px; }
@@ -73,6 +84,7 @@
border-radius: 0;
border: none;
height: auto;
+ width: 100%;
margin: 0;
align-self: center;
}
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 486ad16ea26..49907417e26 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -1,7 +1,7 @@
.awards {
.emoji-icon {
- width: 19px;
- height: 19px;
+ width: 20px;
+ height: 20px;
}
}
@@ -12,10 +12,10 @@
z-index: 9;
width: 300px;
font-size: 14px;
- background-color: $award-emoji-menu-bg;
- border: 1px solid $award-emoji-menu-border;
+ background-color: $white-light;
+ border: 1px solid $border-white-light;
border-radius: $border-radius-base;
- box-shadow: 0 6px 12px rgba(0,0,0,.175);
+ box-shadow: 0 6px 12px $award-emoji-menu-shadow;
pointer-events: none;
opacity: 0;
transform: scale(.2);
@@ -97,8 +97,20 @@
padding: 5px 6px;
outline: 0;
- &:hover,
+ &.disabled {
+ cursor: default;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: $white-light;
+ border-color: $border-color;
+ box-shadow: none;
+ }
+ }
+
&.active,
+ &:hover,
&:active {
background-color: $row-hover;
border-color: $row-hover-border;
@@ -127,7 +139,7 @@
.award-control-icon {
float: left;
margin-right: 5px;
- font-size: 19px;
+ font-size: 18px;
}
.award-control-icon-loading {
@@ -135,6 +147,7 @@
}
.award-control-icon {
- color: $award-emoji-new-btn-icon-color;
+ color: $border-gray-normal;
+ margin-top: 1px;
}
}
diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss
new file mode 100644
index 00000000000..47a8f44c709
--- /dev/null
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -0,0 +1,6 @@
+.badge {
+ font-weight: normal;
+ background-color: $badge-bg;
+ color: $badge-color;
+ vertical-align: baseline;
+}
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index 540718197e0..a2fa2e7769b 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -32,14 +32,14 @@
.blank-state-title {
margin-top: 0;
margin-bottom: 5px;
- font-size: 19px;
+ font-size: 18px;
font-weight: normal;
}
.blank-state-text {
margin-top: 0;
margin-bottom: $gl-padding;
- font-size: 15px;
+ font-size: 14px;
> strong {
font-weight: 600;
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 7e168092522..407c800feb7 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -1,20 +1,15 @@
-.light-well {
- background-color: $background-color;
- padding: 15px;
-}
-
.centered-light-block {
text-align: center;
- color: $gl-gray;
+ color: $gl-text-color;
margin: 20px;
}
.nothing-here-block {
text-align: center;
padding: 20px;
- color: $gl-gray;
+ color: $gl-text-color;
font-weight: normal;
- font-size: 16px;
+ font-size: 14px;
line-height: 36px;
&.diff-collapsed {
@@ -29,19 +24,19 @@
.row-content-block {
margin-top: 0;
margin-bottom: -$gl-padding;
- background-color: $background-color;
+ background-color: $gray-light;
padding: $gl-padding;
margin-bottom: 0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
- color: $gl-gray;
+ color: $gl-text-color;
&.oneline-block {
line-height: 42px;
}
&.white {
- background-color: white;
+ background-color: $white-light;
}
&.top-block {
@@ -123,7 +118,7 @@
.cover-block {
text-align: center;
- background: $background-color;
+ background: $gray-light;
padding-top: 44px;
position: relative;
@@ -140,11 +135,11 @@
}
.cover-title {
- color: $gl-header-color;
+ color: $gl-text-color;
font-size: 23px;
h1 {
- color: $gl-gray-dark;
+ color: $gl-text-color;
margin-bottom: 6px;
font-size: 23px;
}
@@ -158,7 +153,7 @@
p {
padding: 0 $gl-padding;
- color: #5c5d5e;
+ color: $gl-text-color;
}
}
@@ -216,7 +211,7 @@
display: inline;
font-weight: normal;
font-size: 24px;
- color: $gl-title-color;
+ color: $gl-text-color;
}
}
}
@@ -254,3 +249,36 @@
.content-block-small {
padding: 10px 0;
}
+
+.empty-state {
+ margin: 100px 0 0;
+
+ .text-content {
+ max-width: 460px;
+ margin: 0 auto;
+ padding: $gl-padding;
+ }
+
+ .svg-content {
+ text-align: center;
+
+ svg {
+ max-width: 425px;
+ width: 100%;
+ padding: $gl-padding;
+ }
+ }
+
+ .emoji-icon {
+ display: inline-block;
+ }
+
+ @media(max-width: $screen-xs-max) {
+ margin-top: 50px;
+ text-align: center;
+
+ .btn {
+ width: 100%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/broadcast-messages.scss b/app/assets/stylesheets/framework/broadcast-messages.scss
new file mode 100644
index 00000000000..9b54fb94cdc
--- /dev/null
+++ b/app/assets/stylesheets/framework/broadcast-messages.scss
@@ -0,0 +1,21 @@
+.broadcast-message {
+ @extend .alert-warning;
+ padding: 10px;
+ text-align: center;
+
+ div,
+ p {
+ display: inline;
+ margin: 0;
+
+ a {
+ color: inherit;
+ text-decoration: underline;
+ }
+ }
+}
+
+.broadcast-message-preview {
+ @extend .broadcast-message;
+ margin-bottom: 20px;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index e7aff2d0cec..bb6129158d9 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,7 +1,7 @@
@mixin btn-default {
border-radius: 3px;
font-size: $gl-font-size;
- font-weight: 500;
+ font-weight: 400;
padding: $gl-vert-padding $gl-btn-padding;
&:focus,
@@ -15,7 +15,7 @@
@include btn-default;
}
-@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border) {
+@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border, $active-background, $active-border) {
background-color: $background;
color: $text;
border-color: $border;
@@ -23,8 +23,14 @@
&:hover,
&:focus {
background-color: $hover-background;
- color: $hover-text;
border-color: $hover-border;
+ color: $hover-text;
+ }
+
+ &:active {
+ background-color: $active-background;
+ border-color: $active-border;
+ color: $hover-text;
}
}
@@ -62,31 +68,31 @@
}
@mixin btn-green {
- @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #fff);
+ @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, $white-light);
}
@mixin btn-blue {
- @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #fff);
+ @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, $white-light);
}
@mixin btn-blue-medium {
- @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #fff);
+ @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, $white-light);
}
@mixin btn-orange {
- @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #fff);
+ @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, $white-light);
}
@mixin btn-red {
- @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #fff);
+ @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, $white-light);
}
@mixin btn-gray {
- @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, $gl-gray-dark);
+ @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-text-color);
}
@mixin btn-white {
- @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active);
+ @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color);
}
@mixin btn-with-margin {
@@ -139,7 +145,11 @@
&.btn-new,
&.btn-create,
&.btn-save {
- @include btn-outline($white-light, $green-normal, $green-normal, $green-light, $white-light, $green-light);
+ @include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal);
+ }
+
+ &.btn-remove {
+ @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
}
}
@@ -161,11 +171,11 @@
}
&.btn-close {
- @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
+ @include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal);
}
&.btn-spam {
- @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
+ @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal);
}
&.btn-danger,
@@ -195,7 +205,7 @@
}
.fa-caret-down,
- .fa-caret-up {
+ .fa-chevron-down {
margin-left: 5px;
}
@@ -220,12 +230,19 @@
}
}
+.btn-terminal {
+ svg {
+ height: 14px;
+ width: 18px;
+ }
+}
+
.btn-lg {
padding: 12px 20px;
}
.btn-transparent {
- color: $btn-transparent-color;
+ color: $gl-text-color-secondary;
background-color: transparent;
border: 0;
@@ -279,8 +296,8 @@
.active {
box-shadow: $gl-btn-active-background;
- border: 1px solid #c6cacf !important;
- background-color: #e4e7ed !important;
+ border: 1px solid $border-gray-dark !important;
+ background-color: $btn-active-gray-light !important;
}
}
@@ -299,17 +316,17 @@
text-align: left;
padding: 6px 16px;
border-color: $border-color;
- color: $btn-placeholder-gray;
- background-color: $background-color;
+ color: $gray-darkest;
+ background-color: $gray-light;
&:hover,
&:active,
&:focus {
cursor: text;
box-shadow: none;
- border-color: $border-color;
- color: $btn-placeholder-gray;
- background-color: $background-color;
+ border-color: lighten($dropdown-input-focus-border, 20%);
+ color: $gray-darkest;
+ background-color: $gray-light;
}
}
@@ -321,7 +338,7 @@
margin-left: 10px;
i {
- color: $gl-icon-color;
+ color: $gl-text-color-secondary;
}
}
@@ -334,14 +351,20 @@
}
.btn-static {
- background-color: $background-color !important;
- border: 1px solid lightgrey;
+ background-color: $gray-light !important;
+ border: 1px solid $border-gray-normal;
cursor: default;
&:active {
- -moz-box-shadow: inset 0 0 0 white;
- -webkit-box-shadow: inset 0 0 0 white;
- box-shadow: inset 0 0 0 white;
+ -moz-box-shadow: inset 0 0 0 $white-light;
+ -webkit-box-shadow: inset 0 0 0 $white-light;
+ box-shadow: inset 0 0 0 $white-light;
+ }
+}
+
+.btn-inverted {
+ &-secondary {
+ @include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal);
}
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 8642b7530e2..ef921a8c6a9 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -2,7 +2,7 @@
padding-left: 0;
padding-right: 0;
- @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) {
+ @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
overflow-x: scroll;
}
}
@@ -28,13 +28,13 @@
.user-contrib-cell {
&:hover {
cursor: pointer;
- stroke: #000;
+ stroke: $black;
}
}
.user-contrib-text {
font-size: 12px;
- fill: #959494;
+ fill: $calendar-user-contrib-text;
}
.calendar-hint {
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index f3b6ad88ad6..e0e46dd73af 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -11,7 +11,7 @@
padding: $gl-padding;
border-left: 3px solid $border-color;
color: $text-color;
- background: $background-color;
+ background: $gray-light;
}
.bs-callout h4 {
@@ -25,25 +25,25 @@
/* Variations */
.bs-callout-danger {
- background-color: #fdf7f7;
- border-color: #eed3d7;
- color: #b94a48;
+ background-color: $callout-danger-bg;
+ border-color: $callout-danger-border;
+ color: $callout-danger-color;
}
.bs-callout-warning {
- background-color: #faf8f0;
- border-color: #faebcc;
- color: #8a6d3b;
+ background-color: $callout-warning-bg;
+ border-color: $callout-warning-border;
+ color: $callout-warning-color;
}
.bs-callout-info {
- background-color: #f4f8fa;
- border-color: #bce8f1;
- color: #34789a;
+ background-color: $callout-info-bg;
+ border-color: $callout-info-border;
+ color: $callout-info-color;
}
.bs-callout-success {
- background-color: #dff0d8;
- border-color: #5ca64d;
- color: #3c763d;
+ background-color: $callout-success-bg;
+ border-color: $callout-success-border;
+ color: $callout-success-color;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index ad5ac589d0f..0ce94a26a7f 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,9 +1,9 @@
/** COLORS **/
-.cgray { color: $gl-gray; }
-.clgray { color: #bbb; }
-.cred { color: $gl-text-red; }
-.cgreen { color: $gl-text-green; }
-.cdark { color: #444; }
+.cgray { color: $common-gray; }
+.clgray { color: $common-gray-light; }
+.cred { color: $common-red; }
+.cgreen { color: $common-green; }
+.cdark { color: $common-gray-dark; }
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
@@ -26,14 +26,15 @@
.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block; }
.center { text-align: center; }
+.vertical-align-middle { vertical-align: middle; }
.underlined-link { text-decoration: underline; }
-.hint { font-style: italic; color: #999; }
-.light { color: $gl-gray; }
+.hint { font-style: italic; color: $hint-color; }
+.light { color: $common-gray; }
.slead {
- color: $gl-gray;
- font-size: 15px;
+ color: $common-gray;
+ font-size: 14px;
margin-bottom: 12px;
font-weight: normal;
line-height: 24px;
@@ -52,21 +53,38 @@ pre {
}
&.well-pre {
- border: 1px solid #eee;
+ border: 1px solid $well-pre-bg;
background: $gray-light;
border-radius: 0;
- color: #555;
+ color: $well-pre-color;
+ }
+
+ &.wrap {
+ word-break: break-word;
+ white-space: pre-wrap;
}
}
hr {
margin: $gl-padding 0;
+ border-top: 1px solid darken($gray-normal, 8%);
}
.str-truncated {
@include str-truncated;
}
+.block-truncated {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ > div,
+ .str-truncated {
+ display: inline;
+ }
+}
+
.item-title { font-weight: 600; }
/** FLASH message **/
@@ -87,14 +105,14 @@ table a code {
.loading {
margin: 20px auto;
height: 40px;
- color: #555;
+ color: $loading-color;
font-size: 32px;
text-align: center;
}
span.update-author {
display: block;
- color: #999;
+ color: $update-author-color;
font-weight: normal;
font-style: italic;
@@ -105,7 +123,7 @@ span.update-author {
}
.user-mention {
- color: #2fa0bb;
+ color: $user-mention-color;
font-weight: bold;
}
@@ -114,7 +132,7 @@ span.update-author {
}
p.time {
- color: #999;
+ color: $time-color;
font-size: 90%;
margin: 30px 3px 3px 2px;
}
@@ -150,7 +168,7 @@ li.note {
.project_member_show {
td:first-child {
- color: #aaa;
+ color: $project-member-show-color;
}
}
@@ -176,7 +194,7 @@ li.note {
margin-top: 40px;
pre {
- background: white;
+ background: $white-light;
border: none;
font-size: 12px;
}
@@ -184,12 +202,12 @@ li.note {
.error-message {
padding: 10px;
- background: #c67;
+ background: $error-bg;
margin: 0;
- color: #fff;
+ color: $white-light;
a {
- color: #fff;
+ color: $white-light;
text-decoration: underline;
}
}
@@ -197,22 +215,22 @@ li.note {
.browser-alert {
padding: 10px;
text-align: center;
- background: #c67;
- color: #fff;
+ background: $error-bg;
+ color: $white-light;
font-weight: bold;
a {
- color: #fff;
+ color: $white-light;
text-decoration: underline;
}
}
.warning_message {
- border-left: 4px solid #ed9;
- color: #b90;
+ border-left: 4px solid $warning-message-border;
+ color: $warning-message-color;
padding: 10px;
margin-bottom: 10px;
- background: #ffffe6;
+ background: $warning-message-bg;
padding-left: 20px;
&.centered {
@@ -222,7 +240,7 @@ li.note {
.gitlab-promo {
a {
- color: #aaa;
+ color: $gl-promo-color;
margin-right: 30px;
}
}
@@ -245,7 +263,7 @@ li.note {
position: relative;
top: 2px;
left: 5px;
- color: #666;
+ color: $control-group-descr-color;
}
}
}
@@ -255,6 +273,7 @@ img.emoji {
height: 20px;
vertical-align: top;
width: 20px;
+ margin-top: 1px;
}
.chart {
@@ -270,7 +289,7 @@ img.emoji {
table {
td.permission-x {
- background: #d9edf7 !important;
+ background: $table-permission-x-bg !important;
text-align: center;
}
}
@@ -299,6 +318,10 @@ table {
.well {
margin-bottom: $gl-padding;
+
+ hr {
+ border-color: $gray-darker;
+ }
}
.search_box {
@@ -319,13 +342,13 @@ table {
.username {
font-size: 18px;
- color: #666;
+ color: $username-color;
margin-top: 8px;
}
.description {
font-size: $gl-font-size;
- color: #666;
+ color: $description-color;
margin-top: 8px;
}
}
@@ -335,7 +358,7 @@ table {
.profiler-button,
.profiler-controls {
- border-color: #eee !important;
+ border-color: $profiler-border !important;
}
}
@@ -375,4 +398,32 @@ table {
border-top: 1px solid $border-color;
}
-.hide-bottom-border { border-bottom: none !important; }
+.hide-bottom-border {
+ border-bottom: none !important;
+}
+
+.gl-accessibility {
+ &:focus {
+ top: 1px;
+ left: 1px;
+ width: auto;
+ height: 100%;
+ line-height: 50px;
+ padding: 0 10px;
+ clip: auto;
+ text-decoration: none;
+ color: $gl-text-color;
+ background: $gray-light;
+ z-index: 1;
+ }
+}
+
+.str-truncated {
+ &-60 {
+ @include str-truncated(60%);
+ }
+
+ &-100 {
+ @include str-truncated(100%);
+ }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 583c17e4a83..755eddefa42 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -8,6 +8,12 @@
}
}
+@mixin chevron-active {
+ .fa-chevron-down {
+ color: $gray-darkest;
+ }
+}
+
.open {
.dropdown-menu,
.dropdown-menu-nav {
@@ -19,53 +25,32 @@
}
}
+ .dropdown-toggle,
.dropdown-menu-toggle {
- border-color: $dropdown-toggle-hover-border-color;
-
- .fa {
- color: $dropdown-toggle-hover-icon-color;
- }
+ @include chevron-active;
+ border-color: $gray-darkest;
}
}
-.dropdown-menu-toggle {
- position: relative;
- width: 160px;
- padding: 6px 20px 6px 10px;
- background-color: $dropdown-toggle-bg;
- color: $dropdown-toggle-color;
- font-size: 15px;
+.dropdown-toggle {
+ padding: 6px 8px 6px 10px;
+ background-color: $white-light;
+ color: $gl-text-color;
+ font-size: 14px;
text-align: left;
border: 1px solid $border-color;
border-radius: $border-radius-base;
- text-overflow: ellipsis;
white-space: nowrap;
- overflow: hidden;
-
- .fa {
- position: absolute;
- top: 10px;
- right: 8px;
- color: $dropdown-toggle-icon-color;
- &.fa-spinner {
- font-size: 16px;
- margin-top: -8px;
- }
+ &[disabled] {
+ background-color: $input-bg-disabled;
+ cursor: not-allowed;
}
&.no-outline {
outline: 0;
}
- &:hover, {
- border-color: $dropdown-toggle-hover-border-color;
-
- .fa {
- color: $dropdown-toggle-hover-icon-color;
- }
- }
-
&.large {
width: 200px;
}
@@ -86,6 +71,51 @@
max-width: 100%;
padding-right: 25px;
}
+
+ .fa {
+ color: $gray-darkest;
+ }
+
+ .fa-chevron-down {
+ font-size: $dropdown-chevron-size;
+ position: relative;
+ top: -3px;
+ margin-left: 5px;
+ }
+
+ &:hover {
+ @include chevron-active;
+ border-color: $gray-darkest;
+ }
+
+ &:focus:active {
+ @include chevron-active;
+ border-color: $dropdown-toggle-active-border-color;
+ }
+}
+
+.dropdown-menu-toggle {
+ @extend .dropdown-toggle;
+ padding-right: 20px;
+ position: relative;
+ width: 163px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+
+ .fa {
+ position: absolute;
+
+ &.fa-spinner {
+ font-size: 16px;
+ margin-top: -8px;
+ }
+ }
+
+ .fa-chevron-down {
+ position: absolute;
+ top: 11px;
+ right: 8px;
+ }
}
.dropdown-menu,
@@ -98,10 +128,10 @@
width: 240px;
margin-top: 2px;
margin-bottom: 0;
- font-size: 15px;
+ font-size: 14px;
font-weight: normal;
padding: 8px 0;
- background-color: $dropdown-bg;
+ background-color: $white-light;
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
@@ -158,7 +188,6 @@
&.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
- outline: 0;
}
&.dropdown-menu-empty-link {
@@ -172,7 +201,7 @@
}
.icon-play {
- fill: $table-text-gray;
+ fill: $gl-text-color-secondary;
margin-right: 6px;
height: 12px;
width: 11px;
@@ -180,7 +209,7 @@
}
.dropdown-header {
- color: $dropdown-header-color;
+ color: $gl-text-color-secondary;
font-size: 13px;
line-height: 22px;
padding: 0 10px;
@@ -193,7 +222,7 @@
.unclickable {
cursor: not-allowed;
padding: 5px 8px;
- color: $dropdown-header-color;
+ color: $gl-text-color-secondary;
}
}
@@ -351,7 +380,7 @@
position: absolute;
top: 10px;
right: 20px;
- color: #c7c7c7;
+ color: $dropdown-input-fa-color;
font-size: 12px;
pointer-events: none;
}
@@ -504,7 +533,7 @@
.ui-datepicker-calendar {
.ui-state-hover,
.ui-state-active {
- color: #fff;
+ color: $white-light;
border: 0;
}
}
@@ -563,8 +592,8 @@
}
.ui-datepicker-title {
- color: $gl-gray;
- font-size: 15px;
+ color: $gl-text-color;
+ font-size: 14px;
line-height: 1;
font-weight: normal;
}
@@ -572,30 +601,30 @@
th {
padding: 2px 0;
- color: $calendar-header-color;
+ color: $note-disabled-comment-color;
font-weight: normal;
text-transform: lowercase;
border-top: 1px solid $calendar-border-color;
}
.ui-datepicker-unselectable {
- background-color: $calendar-unselectable-bg;
+ background-color: $gray-light;
}
}
.dropdown-menu-inner-title {
display: block;
- color: $gl-title-color;
+ color: $gl-text-color;
font-weight: 600;
}
.dropdown-menu-inner-content {
display: block;
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
}
.dropdown-toggle-text {
&.is-default {
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
}
}
diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index f17797b2381..7158de65143 100644
--- a/app/assets/stylesheets/pages/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -1,4 +1,4 @@
-.emoji-0023-20E3 { background-position: 0 0px; }
+.emoji-0023-20E3 { background-position: 0 0; }
.emoji-002A-20E3 { background-position: -20px 0; }
.emoji-0030-20E3 { background-position: 0 -20px; }
.emoji-0031-20E3 { background-position: -20px -20px; }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index f49d7b92a00..c51912b4ac4 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -19,7 +19,7 @@
.file-title {
position: relative;
- background-color: $background-color;
+ background-color: $gray-light;
border-bottom: 1px solid $border-color;
margin: 0;
text-align: left;
@@ -59,10 +59,10 @@
}
.file-content {
- background: #fff;
+ background: $white-light;
&.image_file {
- background: #eee;
+ background: $file-image-bg;
text-align: center;
img {
@@ -84,8 +84,8 @@
}
&.blob-no-preview {
- background: #eee;
- text-shadow: 0 1px 2px #fff;
+ background: $blob-bg;
+ text-shadow: 0 1px 2px $white-light;
padding: 100px 0;
}
@@ -99,7 +99,7 @@
}
tr {
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid $blame-border;
}
td {
@@ -120,7 +120,7 @@
td.line-numbers {
float: none;
- border-left: 1px solid #ddd;
+ border-left: 1px solid $blame-line-numbers-border;
i {
float: none;
@@ -134,7 +134,7 @@
}
&.logs {
- background: #eee;
+ background: $logs-bg;
max-height: 700px;
overflow-y: auto;
@@ -143,14 +143,14 @@
padding: 10px 0;
border-left: 1px solid $border-color;
margin-bottom: 0;
- background: white;
+ background: $white-light;
li {
- color: #888;
+ color: $logs-li-color;
p {
margin: 0;
- color: #333;
+ color: $logs-p-color;
line-height: 24px;
padding-left: 10px;
}
@@ -182,3 +182,52 @@ span.idiff {
border-bottom-right-radius: 2px;
}
}
+
+.file-stats {
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 10px 0;
+
+ li {
+ padding: 3px 0;
+ line-height: 20px;
+ }
+ }
+
+ .new-file {
+ a {
+ color: $gl-text-green;
+ }
+ }
+
+ .renamed-file {
+ a {
+ color: $gl-text-orange;
+ }
+ }
+
+ .deleted-file {
+ a {
+ color: $gl-text-red;
+ }
+ }
+
+ .edit-file {
+ a {
+ color: $gl-text-color;
+ }
+ }
+
+ a {
+ text-decoration: none;
+
+ .new-file {
+ color: $notify-new-file;
+ }
+
+ .deleted-file {
+ color: $notify-deleted-file;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 19827943385..d957ec64654 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -23,3 +23,118 @@
}
}
+.filtered-search-container {
+ display: -webkit-flex;
+ display: flex;
+}
+
+.filtered-search-input-container {
+ display: -webkit-flex;
+ display: flex;
+ position: relative;
+ width: 100%;
+
+ .form-control {
+ padding-left: 25px;
+ padding-right: 25px;
+
+ &:focus ~ .fa-filter {
+ color: $common-gray-dark;
+ }
+ }
+
+ .fa-filter {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ color: $gray-darkest;
+ }
+
+ .fa-times {
+ right: 10px;
+ color: $gray-darkest;
+ }
+
+ .clear-search {
+ width: 35px;
+ background-color: transparent;
+ border: none;
+ position: absolute;
+ right: 0;
+ height: 100%;
+ outline: none;
+
+ &:hover .fa-times {
+ color: $common-gray-dark;
+ }
+ }
+}
+
+.dropdown-menu .filter-dropdown-item {
+ padding: 0;
+}
+
+.filter-dropdown {
+ max-height: 215px;
+ overflow: auto;
+}
+
+.filter-dropdown-item {
+ .btn {
+ border: none;
+ width: 100%;
+ text-align: left;
+ padding: 8px 16px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ border-radius: 0;
+
+ .fa {
+ width: 15px;
+ }
+
+ .dropdown-label-box {
+ border-color: $white-light;
+ border-style: solid;
+ border-width: 1px;
+ width: 17px;
+ height: 17px;
+ }
+
+ &:hover,
+ &:focus {
+ background-color: $dropdown-hover-color;
+ color: $white-light;
+ text-decoration: none;
+
+ .avatar {
+ border-color: $white-light;
+ }
+ }
+ }
+
+ .dropdown-light-content {
+ font-size: 14px;
+ font-weight: 400;
+ }
+
+ .dropdown-user {
+ display: -webkit-flex;
+ display: flex;
+ }
+
+ .dropdown-user-details {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ }
+}
+
+.hint-dropdown {
+ width: 250px;
+}
+
+.filter-dropdown-loading {
+ padding: 8px 16px;
+}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index a9006de6d3e..eadb9409fee 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -38,7 +38,7 @@
}
}
-@media (max-width: $screen-md-min) {
+@media (max-width: $screen-sm-max) {
ul.notes {
.flash-container.timeline-content {
margin-left: 0;
diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss
deleted file mode 100644
index 5f9685bc71a..00000000000
--- a/app/assets/stylesheets/framework/fonts.scss
+++ /dev/null
@@ -1,45 +0,0 @@
-// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like
-// the way the `src` property is formatted in this file.
-// scss-lint:disable SpaceAfterPropertyColon
-
-/* latin-ext */
-@font-face {
- font-family: 'Source Sans Pro';
- font-style: normal;
- font-weight: 300;
- src:
- local('Source Sans Pro Light'),
- local('SourceSansPro-Light'),
- font-url('SourceSansPro-Light.ttf.woff2') format('woff2'),
- font-url('SourceSansPro-Light.ttf.woff') format('woff');
-}
-@font-face {
- font-family: 'Source Sans Pro';
- font-style: normal;
- font-weight: 400;
- src:
- local('Source Sans Pro'),
- local('SourceSansPro-Regular'),
- font-url('SourceSansPro-Regular.ttf.woff2') format('woff2'),
- font-url('SourceSansPro-Regular.ttf.woff') format('woff');
-}
-@font-face {
- font-family: 'Source Sans Pro';
- font-style: normal;
- font-weight: 600;
- src:
- local('Source Sans Pro Semibold'),
- local('SourceSansPro-Semibold'),
- font-url('SourceSansPro-Semibold.ttf.woff2') format('woff2'),
- font-url('SourceSansPro-Semibold.ttf.woff') format('woff');
-}
-@font-face {
- font-family: 'Source Sans Pro';
- font-style: normal;
- font-weight: 700;
- src:
- local('Source Sans Pro Bold'),
- local('SourceSansPro-Bold'),
- font-url('SourceSansPro-Bold.ttf.woff2') format('woff2'),
- font-url('SourceSansPro-Bold.ttf.woff') format('woff');
-}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index f0727e9688a..25d6fbe465a 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -7,9 +7,9 @@ input {
}
input[type='text'].danger {
- background: #f2dede!important;
- border-color: #d66;
- text-shadow: 0 1px 1px #fff;
+ background: $input-danger-bg !important;
+ border-color: $input-danger-border;
+ text-shadow: 0 1px 1px $white-light;
}
.datetime-controls {
@@ -22,7 +22,7 @@ input[type='text'].danger {
margin-top: 0;
margin-bottom: -$gl-padding;
padding: $gl-padding;
- background-color: $background-color;
+ background-color: $gray-light;
border-top: 1px solid $border-color;
}
@@ -68,6 +68,50 @@ label {
}
}
+.help-form .form-group {
+ margin-left: 0;
+ margin-right: 0;
+
+ .control-label {
+ font-weight: bold;
+ padding-top: 4px;
+ }
+
+ .form-control {
+ height: 29px;
+ background: $white-light;
+ font-family: $monospace_font;
+ }
+
+ .input-group-btn .btn {
+ padding: 3px $gl-btn-padding;
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ }
+
+ .text-block {
+ line-height: 0.8;
+ padding-top: 9px;
+
+ code {
+ line-height: 1.8;
+ }
+
+ img {
+ margin-right: $gl-padding;
+ }
+ }
+
+ @media(max-width: $screen-xs-max) {
+ padding: 0 $gl-padding;
+
+ .control-label,
+ .text-block {
+ padding-left: 0;
+ }
+ }
+}
+
.fieldset-form fieldset {
margin-bottom: 20px;
}
@@ -109,7 +153,7 @@ label {
}
.form-control::-webkit-input-placeholder {
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
}
.input-group {
@@ -119,7 +163,7 @@ label {
}
.input-group-addon {
- background-color: #f7f8fa;
+ background-color: $input-group-addon-bg;
}
.input-group-addon:not(:first-child):not(:last-child) {
@@ -141,7 +185,7 @@ label {
border: 1px solid $green-normal;
&:focus {
- box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal;
+ box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-normal;
border: 0 none;
}
}
@@ -150,7 +194,7 @@ label {
border: 1px solid $red-normal;
&:focus {
- box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6);
+ box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error;
border: 0 none;
}
}
@@ -167,4 +211,3 @@ label {
color: $gl-text-color;
}
}
-
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 91ab1503439..d6566dc4ec9 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -21,7 +21,6 @@
background: $color-darker;
}
- .sidebar-header,
.sidebar-action-buttons {
color: $color-light;
background-color: lighten($color-darker, 5%);
@@ -81,42 +80,65 @@
}
}
+ .about-gitlab {
+ color: $color-light;
+ }
}
}
}
-$theme-charcoal: #3d454d;
-$theme-charcoal-dark: #383f45;
-$theme-charcoal-text: #b9bbbe;
+$theme-charcoal-light: #b9bbbe;
+$theme-charcoal: #485157;
+$theme-charcoal-dark: #3d454d;
+$theme-charcoal-darker: #383f45;
+$theme-blue-light: #becde9;
$theme-blue: #2980b9;
-$theme-graphite: #666;
-$theme-gray: #373737;
+$theme-blue-dark: #1970a9;
+$theme-blue-darker: #096099;
+
+$theme-graphite-light: #ccc;
+$theme-graphite: #777;
+$theme-graphite-dark: #666;
+$theme-graphite-darker: #555;
+
+$theme-black-light: #979797;
+$theme-black: #373737;
+$theme-black-dark: #272727;
+$theme-black-darker: #222;
+
+$theme-green-light: #adc;
$theme-green: #019875;
+$theme-green-dark: #018865;
+$theme-green-darker: #017855;
+
+$theme-violet-light: #98c;
$theme-violet: #548;
+$theme-violet-dark: #436;
+$theme-violet-darker: #325;
body {
&.ui_blue {
- @include gitlab-theme(#becde9, $theme-blue, #1970a9, #096099);
+ @include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker);
}
&.ui_charcoal {
- @include gitlab-theme($theme-charcoal-text, #485157, $theme-charcoal, $theme-charcoal-dark);
+ @include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker);
}
&.ui_graphite {
- @include gitlab-theme(#ccc, #777, $theme-graphite, #555);
+ @include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker);
}
- &.ui_gray {
- @include gitlab-theme(#979797, $theme-gray, #272727, #222);
+ &.ui_black {
+ @include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
}
&.ui_green {
- @include gitlab-theme(#adc, $theme-green, #018865, #017855);
+ @include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker);
}
&.ui_violet {
- @include gitlab-theme(#98c, $theme-violet, #436, #325);
+ @include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker);
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 5a34132112a..24a1ce2b84d 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -8,8 +8,8 @@ header {
&.navbar-empty {
height: $header-height;
- background: #fff;
- border-bottom: 1px solid $btn-gray-hover;
+ background: $white-light;
+ border-bottom: 1px solid $white-normal;
.center-logo {
margin: 8px 0;
@@ -27,7 +27,7 @@ header {
z-index: 100;
margin-bottom: 0;
height: $header-height;
- background-color: $background-color;
+ background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
@@ -45,7 +45,7 @@ header {
padding: 0;
.nav > li > a {
- color: $gl-icon-color;
+ color: $gl-text-color-secondary;
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
@@ -57,21 +57,34 @@ header {
&.header-user-dropdown-toggle {
margin-left: 14px;
+
+ &:hover,
+ &:focus,
+ &:active {
+ .header-user-avatar {
+ border-color: rgba($avatar-border, .2);
+ }
+ }
}
&:hover,
&:focus,
&:active {
- background-color: $background-color;
+ background-color: $gray-light;
+ color: darken($gl-text-color-secondary, 30%);
+
+ .todos-pending-count {
+ background: darken($todo-alert-blue, 10%);
+ }
}
.fa-caret-down {
- font-size: 15px;
+ font-size: 14px;
}
}
.navbar-toggle {
- color: #666;
+ color: $nav-toggle-gray;
margin: 6px 0;
border-radius: 0;
position: absolute;
@@ -79,11 +92,11 @@ header {
padding: 6px 10px;
&:hover {
- background-color: $btn-gray-hover;
+ background-color: $white-normal;
}
&.active {
- color: $gl-icon-color;
+ color: $gl-text-color-secondary;
}
}
}
@@ -95,10 +108,11 @@ header {
font-size: 18px;
padding: 6px 10px;
border: none;
- background-color: $background-color;
+ background-color: $gray-light;
&:hover {
- background-color: $btn-gray-hover;
+ background-color: $white-normal;
+ color: $gl-header-nav-hover-color;
}
}
}
@@ -151,8 +165,8 @@ header {
position: relative;
padding-right: 20px;
margin: 0;
- font-size: 19px;
- max-width: 400px;
+ font-size: 18px;
+ max-width: 385px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
@@ -175,6 +189,7 @@ header {
&:hover {
text-decoration: underline;
+ color: $gl-header-nav-hover-color;
}
}
@@ -191,6 +206,10 @@ header {
font-size: 10px;
text-align: center;
cursor: pointer;
+
+ &:hover {
+ color: $gl-header-nav-hover-color;
+ }
}
.project-item-select {
@@ -218,6 +237,14 @@ header {
}
}
+.page-sidebar-pinned.right-sidebar-expanded {
+ @media (max-width: $screen-md-max) {
+ .header-content .title {
+ width: 300px;
+ }
+ }
+}
+
@media (max-width: $screen-xs-max) {
header .container-fluid {
font-size: 18px;
@@ -254,4 +281,5 @@ header {
float: left;
margin-right: 5px;
border-radius: 50%;
+ border: 1px solid $avatar-border;
}
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 07c8874bf03..909a0f4afda 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -11,7 +11,7 @@
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
- line-height: $code_line_height !important;
+ line-height: 19px;
margin: 0;
overflow: auto;
overflow-y: hidden;
@@ -47,7 +47,7 @@
font-family: $monospace_font;
display: block;
font-size: $code_font_size !important;
- line-height: $code_line_height !important;
+ line-height: 19px;
white-space: nowrap;
i {
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
new file mode 100644
index 00000000000..868f28cd356
--- /dev/null
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -0,0 +1,60 @@
+.ci-status-icon-success {
+ color: $gl-success;
+
+ svg {
+ fill: $gl-success;
+ }
+}
+
+.ci-status-icon-failed {
+ color: $gl-danger;
+
+ svg {
+ fill: $gl-danger;
+ }
+}
+
+.ci-status-icon-pending,
+.ci-status-icon-failed_with_warnings,
+.ci-status-icon-success_with_warnings {
+ color: $gl-warning;
+
+ svg {
+ fill: $gl-warning;
+ }
+}
+
+.ci-status-icon-running {
+ color: $blue-normal;
+
+ svg {
+ fill: $blue-normal;
+ }
+}
+
+.ci-status-icon-canceled,
+.ci-status-icon-disabled,
+.ci-status-icon-not-found {
+ color: $gl-text-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
+}
+
+.ci-status-icon-created,
+.ci-status-icon-skipped {
+ color: $gray-darkest;
+
+ svg {
+ fill: $gray-darkest;
+ }
+}
+
+.ci-status-icon-manual {
+ color: $gl-text-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
+}
diff --git a/app/assets/stylesheets/pages/appearances.scss b/app/assets/stylesheets/framework/images.scss
index 878f44116ba..09a569ad415 100644
--- a/app/assets/stylesheets/pages/appearances.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -4,7 +4,7 @@
}
.appearance-light-logo-preview {
- background-color: $background-color;
+ background-color: $gray-light;
max-width: 72px;
padding: 10px;
margin-bottom: 10px;
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index ba3930e03bd..46632f15f35 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -20,7 +20,7 @@
display: block;
float: left;
margin-right: 10px;
- color: #fff;
+ color: $white-light;
font-size: $gl-font-size;
line-height: 25px;
@@ -37,6 +37,10 @@
}
&.status-box-expired {
- background: #cea61b;
+ background-color: $issue-status-expired;
+ }
+
+ &.status-box-upcoming {
+ background: $gl-text-color-secondary;
}
}
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index 30a5b837d69..18f2f316f02 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -4,13 +4,13 @@
&.ui-datepicker,
&.ui-datepicker-inline {
- border: 1px solid #ddd;
+ border: 1px solid $jq-ui-border;
padding: 10px;
width: 270px;
.ui-datepicker-header {
- background: #fff;
- border-color: #ddd;
+ background: $white-light;
+ border-color: $jq-ui-border;
.ui-datepicker-prev,
.ui-datepicker-next {
@@ -39,7 +39,7 @@
}
&.ui-autocomplete {
- border-color: #ddd;
+ border-color: $jq-ui-border;
padding: 0;
margin-top: 2px;
z-index: 1001;
@@ -50,9 +50,9 @@
}
.ui-state-default {
- border: 1px solid #fff;
- background: #fff;
- color: #777;
+ border: 1px solid $white-light;
+ background: $white-light;
+ color: $jq-ui-default-color;
}
.ui-state-highlight {
@@ -66,7 +66,7 @@
.ui-state-focus {
border: 1px solid $gl-primary;
background: $gl-primary;
- color: #fff;
+ color: $white-light;
}
}
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 7baa4296abf..29d55c44699 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -6,7 +6,7 @@ html {
body {
&.navless {
- background-color: white !important;
+ background-color: $white-light !important;
}
}
@@ -26,6 +26,62 @@ body {
.container-limited {
max-width: $fixed-layout-width;
+
+ &.limit-container-width {
+ max-width: $limited-layout-width;
+ }
+}
+
+.alert-wrapper {
+ .alert {
+ margin-bottom: 0;
+
+ &:last-child {
+ margin-bottom: $gl-padding;
+ }
+ }
+
+ .alert-link-group {
+ float: right;
+ }
+
+ /* Center alert text and alert action links on smaller screens */
+ @media (max-width: $screen-sm-max) {
+ .alert {
+ text-align: center;
+ }
+
+ .alert-link-group {
+ float: none;
+ }
+ }
+
+ /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
+ .alert-warning {
+ transition: background-color 0.15s, border-color 0.15s;
+ background-color: lighten($gl-warning, 4%);
+ border-color: lighten($gl-warning, 4%);
+ }
+
+ .alert-warning + .alert-warning {
+ background-color: $gl-warning;
+ border-color: $gl-warning;
+ }
+
+ .alert-warning + .alert-warning + .alert-warning {
+ background-color: darken($gl-warning, 4%);
+ border-color: darken($gl-warning, 4%);
+ }
+
+ .alert-warning + .alert-warning + .alert-warning + .alert-warning {
+ background-color: darken($gl-warning, 8%);
+ border-color: darken($gl-warning, 8%);
+ }
+
+ .alert-warning:only-of-type {
+ background-color: $gl-warning;
+ border-color: $gl-warning;
+ }
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index bc0610cc417..1c6698ad0c6 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -11,8 +11,8 @@
> li {
padding: 10px 15px;
min-height: 20px;
- border-bottom: 1px solid #eee;
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+ border-bottom: 1px solid $list-border-light;
+ border-bottom: 1px solid $list-border;
&::after {
content: " ";
@@ -21,7 +21,7 @@
}
&.disabled {
- color: #888;
+ color: $list-text-disabled-color;
}
&.unstyled {
@@ -31,12 +31,12 @@
}
&.warning-row {
- background-color: #fcf8e3;
- border-color: #faebcc;
- color: #8a6d3b;
+ background-color: $list-warning-row-bg;
+ border-color: $list-warning-row-border;
+ color: $list-warning-row-color;
}
- &.smoke { background-color: $background-color; }
+ &.smoke { background-color: $gray-light; }
&:not(.ui-sort-disabled):hover {
background: $row-hover;
@@ -46,7 +46,7 @@
border-bottom: none;
&.bottom {
- background: $background-color;
+ background: $gray-light;
}
}
@@ -59,7 +59,7 @@
p {
padding-top: 1px;
margin: 0;
- color: $gray-dark;
+ color: $white-normal;
img {
position: relative;
@@ -106,14 +106,14 @@ ul.task-list {
}
}
+// Generic content list
ul.content-list {
@include basic-list;
-
margin: 0;
padding: 0;
- > li {
- border-color: $table-border-color;
+ li {
+ border-color: $white-normal;
font-size: $list-font-size;
color: $list-text-color;
@@ -128,7 +128,7 @@ ul.content-list {
}
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
.member-group-link {
@@ -163,6 +163,10 @@ ul.content-list {
&:last-child {
margin-right: 0;
+
+ @media(max-width: $screen-xs-max) {
+ margin: 0 auto;
+ }
}
}
@@ -186,25 +190,97 @@ ul.content-list {
&.list-placeholder {
background-color: $gray-light;
- border: dotted 1px $gray-dark;
+ border: dotted 1px $white-normal;
margin: 1px 0;
min-height: 52px;
}
}
}
-.panel > .content-list > li {
- padding: $gl-padding-top $gl-padding;
+// Content list using flexbox
+.flex-list {
+ .flex-row {
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
+ }
+
+ .row-main-content {
+ flex: 1 1 auto;
+ overflow: hidden;
+ padding-right: 8px;
+ }
+
+ .row-fixed-content {
+ flex: 0 0 auto;
+ margin-left: auto;
+ }
+
+ .row-title {
+ font-weight: 600;
+ }
+
+ .row-second-line {
+ display: block;
+ }
+
+ .dropdown {
+ .btn-block {
+ margin-bottom: 0;
+ line-height: inherit;
+ }
+ }
+
+ .label-default {
+ color: $gl-text-color-secondary;
+ }
+}
+
+// Table list
+.table-list {
+ display: table;
+ width: 100%;
+
+ .table-list-row {
+ display: table-row;
+ }
+
+ .table-list-cell {
+ display: table-cell;
+ vertical-align: top;
+ padding: 10px 16px;
+ border-bottom: 1px solid $gray-darker;
- &.commit {
- @media (min-width: $screen-sm-min) {
- padding-left: 46px + $gl-padding;
+ &.avatar-cell {
+ width: 36px;
+ padding-right: 0;
+
+ img {
+ margin-right: 0;
+ }
}
}
+
+ &.table-wide {
+ .table-list-cell {
+ &:last-of-type {
+ padding-right: 0;
+ }
+
+ &:first-of-type {
+ padding-left: 0;
+ }
+ }
+ }
+}
+
+.panel > .content-list > li {
+ padding: $gl-padding-top $gl-padding;
}
ul.controls {
- padding-top: 1px;
float: right;
list-style: none;
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 6d28d98b283..5bff694658c 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -56,26 +56,24 @@
.md-header {
.nav-links {
- .active {
- a {
- border-bottom-color: #000;
- }
- }
-
a {
padding-top: 0;
- line-height: 1;
- border-bottom: 1px solid $border-color;
+ line-height: 19px;
&.btn.btn-xs {
padding: 2px 5px;
}
+
+ &:focus {
+ margin-top: -10px;
+ padding-top: 10px;
+ }
}
}
}
.referenced-users {
- color: #4c4e54;
+ color: $gl-text-color;
padding-top: 10px;
}
@@ -87,8 +85,8 @@
.markdown-area {
border-radius: 0;
- background: #fff;
- border: 1px solid #ddd;
+ background: $white-light;
+ border: 1px solid $md-area-border;
min-height: 140px;
max-height: 500px;
padding: 5px;
@@ -110,13 +108,13 @@
hr {
// Darken 'whitesmoke' a bit to make it more visible in note bodies
- border-color: darken(#f5f5f5, 8%);
+ border-color: darken($gray-normal, 8%);
margin: 10px 0;
}
// Border around images in issue and MR comments.
img:not(.emoji) {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
padding: 5px;
margin: 5px 0;
// Ensure that image does not exceed viewport
@@ -137,7 +135,7 @@
.toolbar-btn {
float: left;
padding: 0 5px;
- color: #959494;
+ color: $gl-text-color-secondary;
background: transparent;
border: 0;
outline: 0;
@@ -148,7 +146,19 @@
}
}
-.atwho-view small.description {
- float: right;
- padding: 3px 5px;
+.atwho-view {
+ small.description {
+ float: right;
+ padding: 3px 5px;
+ }
+
+ .avatar-inline {
+ margin-bottom: 0;
+ }
+
+ .cur {
+ .avatar {
+ border: 1px solid $white-light;
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index f84ca36d10f..1acd06122a3 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -24,7 +24,7 @@
@include clearfix;
padding: 10px 0;
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid $list-border-light;
display: block;
margin: 0;
@@ -46,7 +46,7 @@
&.light {
a {
- color: $gl-gray;
+ color: $gl-text-color;
}
}
}
@@ -67,8 +67,8 @@
}
@mixin dark-diff-match-line {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ color: $dark-diff-match-bg;
+ background: $dark-diff-match-color;
}
@mixin webkit-prefix($property, $value) {
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index c1ed43bc20f..8e2c56a8488 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -23,21 +23,21 @@
margin-right: 0;
}
- .issues-details-filters,
+ .issues-details-filters:not(.filtered-search-block),
.dash-projects-filters,
.check-all-holder {
display: none;
}
- .rss-btn {
+ .issues-holder .issue-check {
display: none;
}
- .project-home-links {
+ .rss-btn {
display: none;
}
- .project-avatar {
+ .project-home-links {
display: none;
}
@@ -54,7 +54,7 @@
}
// Display Star and Fork buttons without counters on mobile.
- .project-action-buttons {
+ .project-repo-buttons {
display: block;
.count-buttons .btn {
@@ -71,7 +71,7 @@
display: none;
}
- .group-right-buttons {
+ .group-buttons {
display: none;
}
@@ -133,9 +133,9 @@
right: 0;
top: 30%;
padding: 5px 15px;
- background: #eee;
+ background: $show-aside-bg;
font-size: 20px;
- color: #777;
+ color: $show-aside-color;
z-index: 100;
- box-shadow: 0 1px 2px #ddd;
+ box-shadow: 0 1px 2px $show-aside-shadow;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index ce864c2de5e..401c2d0f6ee 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -49,50 +49,60 @@
padding: $gl-btn-padding;
padding-bottom: 11px;
margin-bottom: -1px;
- font-size: 15px;
+ font-size: 14px;
line-height: 28px;
- color: #959494;
+ color: $gl-text-color-secondary;
border-bottom: 2px solid transparent;
&:hover,
&:active,
&:focus {
text-decoration: none;
+ border-bottom: 2px solid $gray-darkest;
+ color: $black;
+
+ .badge {
+ color: $black;
+ }
}
}
&.active a {
border-bottom: 2px solid $link-underline-blue;
color: $black;
- }
+ font-weight: 600;
- .badge {
- font-weight: normal;
- background-color: #eee;
- color: $btn-transparent-color;
- vertical-align: baseline;
+ .badge {
+ color: $black;
+ }
}
}
&.sub-nav {
text-align: center;
- background-color: $dark-background-color;
+ background-color: $gray-normal;
.container-fluid {
- background-color: $dark-background-color;
+ background-color: $gray-normal;
margin-bottom: 0;
}
li {
+ &.active a {
+ border-bottom: none;
+ color: $link-underline-blue;
+ }
+
a {
margin: 0;
padding: 11px 10px 9px;
- }
- &.active a {
- border-bottom: none;
- color: $link-underline-blue;
+ &:hover,
+ &:active,
+ &:focus {
+ border-color: transparent;
+ }
}
}
}
@@ -100,17 +110,17 @@
.top-area {
@include clearfix;
- border-bottom: 1px solid $btn-gray-hover;
+ border-bottom: 1px solid $white-normal;
.nav-text {
padding-top: 16px;
padding-bottom: 11px;
display: inline-block;
- width: 50%;
line-height: 28px;
+ white-space: normal;
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
width: 100%;
}
}
@@ -148,30 +158,24 @@
}
.nav-controls {
- width: 50%;
display: inline-block;
float: right;
text-align: right;
padding: 11px 0;
margin-bottom: 0;
- > .dropdown {
- margin-right: $gl-padding-top;
- display: inline-block;
- vertical-align: top;
-
- &:last-child {
- margin-right: 0;
- }
- }
-
- > .btn {
+ > .btn,
+ > .btn-container,
+ > .dropdown,
+ > input,
+ > form {
margin-right: $gl-padding-top;
display: inline-block;
vertical-align: top;
&:last-child {
margin-right: 0;
+ float: right;
}
}
@@ -179,19 +183,21 @@
float: none;
}
- > form {
- display: inline-block;
- }
-
.icon-label {
display: none;
}
- input {
+ .btn,
+ .dropdown,
+ .dropdown-toggle,
+ input,
+ form {
height: 35px;
+ }
+
+ input {
display: inline-block;
position: relative;
- margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */
@media (min-width: $screen-md-min) { width: 200px; }
@@ -215,6 +221,7 @@
.btn,
form,
.dropdown,
+ .dropdown-toggle,
.dropdown-menu-toggle,
.form-control {
margin: 0 0 10px;
@@ -253,8 +260,26 @@
.nav-text,
.nav-controls {
width: auto;
+
+ @media (max-width: $screen-xs-max) {
+ width: 100%;
+ }
+ }
+ }
+
+ &.multi-line {
+ .nav-text {
+ line-height: 20px;
+ }
+
+ .nav-controls {
+ padding: 17px 0;
}
}
+
+ pre {
+ width: 100%;
+ }
}
.layout-nav {
@@ -262,7 +287,7 @@
top: $header-height;
width: 100%;
z-index: 11;
- background: $background-color;
+ background: $gray-light;
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
text-align: center;
@@ -290,7 +315,7 @@
.fa-caret-down {
margin-left: 5px;
- color: $gl-icon-color;
+ color: $gl-text-color-secondary;
}
.dropdown {
@@ -310,37 +335,9 @@
height: 51px;
li {
-
a {
padding-top: 10px;
}
-
- a,
- i {
- color: $layout-link-gray;
- }
-
- &.active {
-
- a,
- i {
- color: $black;
- }
-
- svg {
- path,
- polygon {
- fill: $black;
- }
- }
- }
-
- &:hover {
- a,
- i {
- color: $black;
- }
- }
}
}
}
@@ -353,7 +350,7 @@
}
.fade-right {
- @include fade(left, $background-color);
+ @include fade(left, $gray-light);
right: -5px;
.fa {
@@ -362,7 +359,7 @@
}
.fade-left {
- @include fade(right, $background-color);
+ @include fade(right, $gray-light);
left: -5px;
.fa {
@@ -373,7 +370,7 @@
&.sub-nav-scroll {
.fade-right {
- @include fade(left, $dark-background-color);
+ @include fade(left, $gray-normal);
right: 0;
.fa {
@@ -382,7 +379,7 @@
}
.fade-left {
- @include fade(right, $dark-background-color);
+ @include fade(right, $gray-normal);
left: 0;
.fa {
@@ -436,3 +433,40 @@
}
}
}
+
+@media (max-width: $screen-xs-max) {
+ .top-area {
+ flex-flow: row wrap;
+
+ .nav-controls {
+ $controls-margin: $btn-xs-side-margin - 2px;
+ flex: 0 0 100%;
+
+ &.controls-flex {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: center;
+ padding: 0 0 $gl-padding-top;
+ }
+
+ .controls-item,
+ .controls-item-full,
+ .controls-item:last-child {
+ flex: 1 1 35%;
+ display: block;
+ width: 100%;
+ margin: $controls-margin;
+
+ .btn,
+ .dropdown {
+ margin: 0;
+ }
+ }
+
+ .controls-item-full {
+ flex: 1 1 100%;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss
new file mode 100644
index 00000000000..5f4211147f3
--- /dev/null
+++ b/app/assets/stylesheets/framework/page-header.scss
@@ -0,0 +1,62 @@
+.page-content-header {
+ line-height: 34px;
+ padding: 10px 0;
+ margin-bottom: 0;
+
+ @media (min-width: $screen-sm-min) {
+ display: flex;
+ align-items: center;
+
+ .header-main-content {
+ flex: 1;
+ }
+ }
+
+ .header-action-buttons {
+ i {
+ color: $gl-text-color-secondary;
+ font-size: 13px;
+ margin-right: 3px;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .btn {
+ width: 100%;
+ margin-top: 10px;
+ }
+
+ .dropdown {
+ width: 100%;
+ }
+ }
+ }
+
+ .avatar {
+ @extend .avatar-inline;
+ margin-left: 0;
+
+ @media (min-width: $screen-sm-min) {
+ margin-left: 4px;
+ }
+ }
+
+ .commit-committer-link,
+ .commit-author-link {
+ color: $gl-text-color;
+ font-weight: bold;
+ }
+
+ .commit-info {
+ &.branches {
+ margin-left: 8px;
+ }
+ }
+
+ .ci-status-link {
+ svg {
+ position: relative;
+ top: 2px;
+ margin: 0 2px 0 3px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index cb2c351c368..b37c1d0d670 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -43,7 +43,7 @@
/**
* Small screen pagination
*/
-@media (max-width: $screen-xs) {
+@media (max-width: $screen-xs-min) {
.gl-pagination {
.pagination li a {
padding: 6px 10px;
@@ -62,7 +62,7 @@
/**
* Medium screen pagination
*/
-@media (min-width: $screen-xs) and (max-width: $screen-md-max) {
+@media (min-width: $screen-xs-min) and (max-width: $screen-md-max) {
.gl-pagination {
.page {
display: none;
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index 5ba0486177f..efe93724013 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -18,6 +18,20 @@
margin-top: -2px;
margin-left: 5px;
}
+
+ &.split {
+ display: flex;
+ align-items: center;
+ }
+
+ .left {
+ flex: 1 1 auto;
+ }
+
+ .right {
+ flex: 0 0 auto;
+ text-align: right;
+ }
}
.panel-body {
@@ -34,3 +48,11 @@
line-height: inherit;
}
}
+
+.panel-default {
+ .table-list-row:last-child {
+ .table-list-cell {
+ border-bottom: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 920ce249b9a..9ab17e67d4c 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -6,7 +6,7 @@
.select2-container,
.select2-container.select2-drop-above {
.select2-choice {
- background: #fff;
+ background: $white-light;
border-color: $input-border;
height: 35px;
padding: $gl-vert-padding $gl-input-padding;
@@ -39,7 +39,7 @@
}
&:hover {
- background-color: $gray-dark;
+ background-color: $white-normal;
border-color: $border-white-normal;
color: $gl-text-color;
}
@@ -47,7 +47,7 @@
}
.select2-drop {
- box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0;
+ box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0;
border-radius: $border-radius-default;
border: none;
min-width: 175px;
@@ -59,7 +59,7 @@
}
.select2-drop {
- color: #7f8fa4;
+ color: $gl-grayish-blue;
}
.select2-highlighted {
@@ -108,7 +108,7 @@
border-color: $input-border;
color: $gl-text-color;
line-height: 15px;
- background-color: $background-color;
+ background-color: $gray-light;
background-image: none;
.select2-search-choice-close {
@@ -156,7 +156,7 @@
.select2-search input {
padding: 2px 25px 2px 5px;
- background: #fff image-url('select2.png');
+ background: $white-light image-url('select2.png');
background-repeat: no-repeat;
background-position: right 0 bottom 6px;
border: 1px solid $input-border;
@@ -169,7 +169,7 @@
}
.select2-search input.select2-active {
- background-color: #fff;
+ background-color: $white-light;
background-image: image-url('select2-spinner.gif') !important;
background-repeat: no-repeat;
background-position: right 5px center !important;
@@ -206,7 +206,7 @@
.select2-highlighted {
.group-result {
.group-path {
- color: #fff;
+ color: $white-light;
}
}
}
@@ -221,7 +221,7 @@
}
.group-path {
- color: #999;
+ color: $group-path-color;
}
}
@@ -241,7 +241,7 @@
.namespace-result {
.namespace-kind {
- color: #aaa;
+ color: $namespace-kind-color;
font-weight: normal;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 44c445c0543..f0b03710c79 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -23,7 +23,7 @@
.sidebar-wrapper {
z-index: 1000;
- background: $background-color;
+ background: $gray-light;
.nicescroll-rails-hr {
// TODO: Figure out why nicescroll doesn't hide horizontal bar
@@ -36,7 +36,7 @@
transition: padding $sidebar-transition-duration;
.container-fluid {
- background: #fff;
+ background: $white-light;
padding: 0 $gl-padding;
&.container-blank {
@@ -59,11 +59,6 @@
padding: 0 !important;
}
- .sidebar-header {
- padding: 11px 22px 12px;
- font-size: 20px;
- }
-
li {
&.separate-item {
padding-top: 10px;
@@ -106,6 +101,17 @@
padding: 0 8px;
border-radius: 6px;
}
+
+ .about-gitlab {
+ padding: 7px $gl-sidebar-padding;
+ font-size: $gl-font-size;
+ line-height: 24px;
+ display: block;
+ text-decoration: none;
+ font-weight: normal;
+ position: absolute;
+ bottom: 10px;
+ }
}
.sidebar-action-buttons {
@@ -177,7 +183,9 @@
&.right-sidebar-expanded {
.line-resolve-all-container {
- display: none;
+ @media (min-width: $sidebar-breakpoint) {
+ display: none;
+ }
}
}
}
@@ -220,7 +228,7 @@ header.header-sidebar-pinned {
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- &:not(.build-sidebar) {
+ &:not(.build-sidebar):not(.wiki-sidebar) {
padding-right: $sidebar_collapsed_width;
}
}
@@ -228,9 +236,13 @@ header.header-sidebar-pinned {
@media (min-width: $screen-md-min) {
padding-right: $gutter_width;
- .merge-request-tabs-holder.affix {
+ &:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width;
}
+
+ &.with-overlay .merge-request-tabs-holder.affix {
+ right: $sidebar_collapsed_width;
+ }
}
&.with-overlay {
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 857eb76131a..5f7e1b17cc7 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -1,3 +1,13 @@
+.snippet-row {
+ .title {
+ margin-bottom: 2px;
+ }
+
+ .snippet-filename {
+ padding: 0 2px;
+ }
+}
+
.snippet-form-holder .file-holder .file-title {
padding: 2px;
}
@@ -12,23 +22,19 @@
.snippet-file-content {
border-radius: 3px;
- margin-bottom: $gl-padding;
-
- .btn-clipboard {
- @extend .btn;
- }
}
-.project-snippets .awards {
- border-bottom: 1px solid $table-border-color;
- padding-bottom: $gl-padding;
+.snippet-header {
+ padding: $gl-padding 0;
}
.snippet-title {
font-size: 24px;
font-weight: 600;
- padding: $gl-padding;
- padding-left: 0;
+}
+
+.snippet-edited-ago {
+ color: $gray-darkest;
}
.snippet-actions {
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 9a90d3794fd..6d9fa74a030 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -14,11 +14,11 @@ table {
.warning,
.danger,
.info {
- color: #fff;
+ color: $white-light;
a:not(.btn) {
text-decoration: underline;
- color: #fff;
+ color: $white-light;
}
}
@@ -31,14 +31,31 @@ table {
}
th {
- background-color: $background-color;
+ background-color: $gray-light;
font-weight: normal;
border-bottom: none;
+
+ &.wide {
+ width: 55%;
+ }
}
td {
- border-color: $table-border-color;
+ border-color: $white-normal;
}
}
}
}
+
+.responsive-table {
+ @media (max-width: $screen-sm-max) {
+ th {
+ width: 100%;
+ }
+
+ td {
+ width: 100%;
+ float: left;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 875cded8b4e..ff185cd8767 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -6,8 +6,8 @@
.timeline-entry {
padding: $gl-padding $gl-btn-padding 11px;
- border-color: $table-border-color;
- color: $gl-gray;
+ border-color: $white-normal;
+ color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
&:target {
@@ -32,7 +32,7 @@
.system-note {
.note-text {
- color: $gl-gray !important;
+ color: $gl-text-color !important;
}
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 59f4594bb83..12d56359d7d 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -91,19 +91,19 @@
// Labels
.label {
padding: 4px 5px;
- font-size: 13px;
+ font-size: 12px;
font-style: normal;
font-weight: normal;
display: inline-block;
&.label-gray {
- background-color: #f8fafc;
- color: $gl-gray;
+ background-color: $label-gray-bg;
+ color: $gl-text-color;
text-shadow: none;
}
&.label-inverse {
- background-color: #333;
+ background-color: $label-inverse-bg;
}
}
@@ -158,7 +158,7 @@
font-weight: normal;
a {
- color: #777;
+ color: $panel-heading-link-color;
}
}
}
@@ -172,7 +172,7 @@
.alert {
a:not(.btn) {
@extend .alert-link;
- color: #fff;
+ color: $white-light;
text-decoration: underline;
}
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 44fe37d3a4a..0fc89d5976a 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -48,7 +48,7 @@ $font-size-base: $gl-font-size;
$padding-base-vertical: $gl-vert-padding;
$padding-base-horizontal: $gl-padding;
-$component-active-color: #fff;
+$component-active-color: $white-light;
$component-active-bg: $brand-info;
//== Forms
@@ -65,20 +65,20 @@ $legend-color: $text-color;
//
//##
-$pagination-color: $gl-gray;
-$pagination-bg: #fff;
+$pagination-color: $gl-text-color;
+$pagination-bg: $white-light;
$pagination-border: $border-color;
-$pagination-hover-color: $gl-gray;
+$pagination-hover-color: $gl-text-color;
$pagination-hover-bg: $row-hover;
$pagination-hover-border: $border-color;
$pagination-active-color: $blue-dark;
-$pagination-active-bg: #fff;
+$pagination-active-bg: $white-light;
$pagination-active-border: $border-color;
$pagination-disabled-color: #cdcdcd;
-$pagination-disabled-bg: $background-color;
+$pagination-disabled-bg: $gray-light;
$pagination-disabled-border: $border-color;
@@ -86,19 +86,19 @@ $pagination-disabled-border: $border-color;
//
//## Define colors for form feedback states and, by default, alerts.
-$state-success-text: #fff;
+$state-success-text: $white-light;
$state-success-bg: $brand-success;
$state-success-border: $brand-success;
-$state-info-text: #fff;
+$state-info-text: $white-light;
$state-info-bg: $brand-info;
$state-info-border: $brand-info;
-$state-warning-text: #fff;
+$state-warning-text: $white-light;
$state-warning-bg: $brand-warning;
$state-warning-border: $brand-warning;
-$state-danger-text: #fff;
+$state-danger-text: $white-light;
$state-danger-bg: $brand-danger;
$state-danger-border: $brand-danger;
@@ -117,10 +117,13 @@ $alert-border-radius: 0;
$panel-border-radius: 2px;
$panel-default-text: $text-color;
$panel-default-border: $border-color;
-$panel-default-heading-bg: $background-color;
-$panel-footer-bg: $background-color;
+$panel-default-heading-bg: $gray-light;
+$panel-footer-bg: $gray-light;
$panel-inner-border: $border-color;
+$badge-bg: $badge-bg;
+$badge-color: $badge-color;
+
//== Wells
//
//##
@@ -135,14 +138,14 @@ $well-border: #eee;
$code-color: #c7254e;
$code-bg: #f9f2f4;
-$kbd-color: #fff;
+$kbd-color: $white-light;
$kbd-bg: #333;
//== Buttons
//
//##
$btn-default-color: $gl-text-color;
-$btn-default-bg: #fff;
+$btn-default-bg: $white-light;
$btn-default-border: #e7e9ed;
//== Nav
@@ -153,8 +156,8 @@ $nav-link-padding: 13px $gl-padding;
//== Code
//
//##
-$pre-bg: $background-color !default;
-$pre-color: $gl-gray !default;
+$pre-bg: $gray-light !default;
+$pre-color: $gl-text-color !default;
$pre-border-color: $border-color;
-$table-bg-accent: $background-color;
+$table-bg-accent: $gray-light;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 070e42d63d2..54958973f15 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -1,5 +1,5 @@
@mixin md-typography {
- color: $md-text-color;
+ color: $gl-text-color;
word-wrap: break-word;
a {
@@ -10,7 +10,7 @@
max-width: 100%;
}
- *:first-child {
+ *:first-child:not(.katex-display) {
margin-top: 0;
}
@@ -33,15 +33,15 @@
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
- color: #555;
+ color: $kdb-color;
vertical-align: middle;
- background-color: #fcfcfc;
+ background-color: $kdb-bg;
border-width: 1px;
border-style: solid;
- border-color: #ccc #ccc #bbb;
+ border-color: $kdb-border $kdb-border $kdb-border-bottom;
border-image: none;
border-radius: 3px;
- box-shadow: 0 -1px 0 #bbb inset;
+ box-shadow: 0 -1px 0 $kdb-shadow inset;
}
h1 {
@@ -50,14 +50,14 @@
margin: 16px 0 10px;
padding: 0 0 0.3em;
border-bottom: 1px solid $white-dark;
- color: $gl-gray-dark;
+ color: $gl-text-color;
}
h2 {
font-size: 1.5em;
font-weight: 600;
margin: 16px 0 10px;
- color: $gl-gray-dark;
+ color: $gl-text-color;
}
h3 {
@@ -81,7 +81,7 @@
}
blockquote {
- color: #7f8fa4;
+ color: $gl-grayish-blue;
font-size: inherit;
padding: 8px 21px;
margin: 12px 0;
@@ -94,13 +94,13 @@
}
blockquote p {
- color: #7f8fa4 !important;
+ color: $gl-grayish-blue !important;
font-size: inherit;
line-height: 1.5;
}
p {
- color: #5c5d5e;
+ color: $gl-text-color;
margin: 6px 0 0;
}
@@ -108,10 +108,10 @@
@extend .table;
@extend .table-bordered;
margin: 12px 0;
- color: #5c5d5e;
+ color: $gl-text-color;
th {
- background: #f8fafc;
+ background: $label-gray-bg;
}
}
@@ -182,6 +182,7 @@
left: -16px;
position: absolute;
text-decoration: none;
+ outline: none;
&::after {
content: image-url('icon_anchor.svg');
@@ -201,7 +202,7 @@
*
*/
body {
- -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px;
+ -webkit-text-shadow: $body-text-shadow 0 0 1px;
}
.page-title {
@@ -229,7 +230,7 @@ h3,
h4,
h5,
h6 {
- color: $gl-title-color;
+ color: $gl-text-color;
font-weight: 600;
}
@@ -291,7 +292,7 @@ h2,
h3,
h4 {
small {
- color: $gl-gray;
+ color: $gl-text-color;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index e0d00759c9c..07cb669a46e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -5,77 +5,77 @@ $sidebar_collapsed_width: 62px;
$sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
-$gutter_inner_width: 258px;
+$gutter_inner_width: 250px;
$sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px;
/*
* Color schema
*/
+$darken-normal-factor: 7%;
+$darken-dark-factor: 10%;
+$darken-border-factor: 5%;
+
$white-light: #fff;
-$white-normal: #ededed;
-$white-dark: #ececec;
+$white-normal: #f0f0f0;
+$white-dark: #eaeaea;
$gray-lightest: #fdfdfd;
$gray-light: #fafafa;
$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
-$gray-dark: #ededed;
+$gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
-$gray-darkest: #c9c9c9;
+$gray-darkest: #c4c4c4;
-$green-light: #38ae67;
-$green-normal: #2faa60;
-$green-dark: #2ca05b;
+$green-light: #3cbd70;
+$green-normal: darken($green-light, $darken-normal-factor);
+$green-dark: darken($green-light, $darken-dark-factor);
$blue-light: #2ea8e5;
-$blue-normal: #2d9fd8;
-$blue-dark: #2897ce;
+$blue-normal: darken($blue-light, $darken-normal-factor);
+$blue-dark: darken($blue-light, $darken-dark-factor);
$blue-medium-light: #3498cb;
-$blue-medium: #2f8ebf;
-$blue-medium-dark: #2d86b4;
+$blue-medium: darken($blue-medium-light, $darken-normal-factor);
+$blue-medium-dark: darken($blue-medium-light, $darken-dark-factor);
$blue-light-transparent: rgba(44, 159, 216, 0.05);
$orange-light: #fc8a51;
-$orange-normal: #e75e40;
-$orange-dark: #ce5237;
+$orange-normal: darken($orange-light, $darken-normal-factor);
+$orange-dark: darken($orange-light, $darken-dark-factor);
$red-light: #e52c5a;
-$red-normal: #d22852;
-$red-dark: darken($red-normal, 5%);
+$red-normal: darken($red-light, $darken-normal-factor);
+$red-dark: darken($red-light, $darken-dark-factor);
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
-$border-white-light: #f1f2f4;
-$border-white-normal: #d6dae2;
-$border-white-dark: #c6cacf;
+$border-white-light: darken($white-light, $darken-border-factor);
+$border-white-normal: darken($white-normal, $darken-border-factor);
-$border-gray-light: #dcdcdc;
-$border-gray-normal: #d7d7d7;
-$border-gray-dark: #c6cacf;
+$border-gray-light: darken($gray-light, $darken-border-factor);
+$border-gray-normal: darken($gray-normal, $darken-border-factor);
+$border-gray-dark: darken($white-normal, $darken-border-factor);
$border-green-extra-light: #9adb84;
-$border-green-light: #2faa60;
-$border-green-normal: #2ca05b;
-$border-green-dark: #279654;
-
-$border-blue-light: #2d9fd8;
-$border-blue-normal: #2897ce;
-$border-blue-dark: #258dc1;
+$border-green-light: darken($green-light, $darken-border-factor);
+$border-green-normal: darken($green-normal, $darken-border-factor);
+$border-green-dark: darken($green-dark, $darken-border-factor);
-$border-orange-light: #fc6d26;
-$border-orange-normal: #ce5237;
-$border-orange-dark: #c14e35;
+$border-blue-light: darken($blue-light, $darken-border-factor);
+$border-blue-normal: darken($blue-normal, $darken-border-factor);
+$border-blue-dark: darken($blue-dark, $darken-border-factor);
-$border-red-light: #d22852;
-$border-red-normal: #ca264f;
-$border-red-dark: darken($border-red-normal, 5%);
+$border-orange-light: darken($orange-light, $darken-border-factor);
+$border-orange-normal: darken($orange-normal, $darken-border-factor);
+$border-orange-dark: darken($orange-dark, $darken-border-factor);
-$help-well-bg: $gray-light;
-$help-well-border: #e5e5e5;
+$border-red-light: darken($red-light, $darken-border-factor);
+$border-red-normal: darken($red-normal, $darken-border-factor);
+$border-red-dark: darken($red-dark, $darken-border-factor);
$warning-message-bg: #fbf2d9;
$warning-message-color: #9e8e60;
@@ -86,52 +86,54 @@ $warning-message-border: #f0e2bb;
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
-$table-border-color: #f0f0f0;
-$background-color: $gray-light;
-$dark-background-color: #f5f5f5;
-$table-text-gray: #8f8f8f;
-$widget-expand-item: #e8f2f7;
-$widget-inner-border: #eef0f2;
+$sidebar-collapsed-icon-color: #999;
+$well-expand-item: #e8f2f7;
+$well-inner-border: #eef0f2;
+$well-light-border: #f1f1f1;
+$well-light-text-color: #5b6169;
/*
* Text
*/
-$gl-font-size: 15px;
-$gl-title-color: #333;
-$gl-text-color: #5c5c5c;
-$gl-text-color-light: #8c8c8c;
+$gl-font-size: 14px;
+$gl-text-color: rgba(0, 0, 0, .85);
+$gl-text-color-secondary: rgba(0, 0, 0, .55);
+$gl-text-color-disabled: rgba(0, 0, 0, .35);
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
$gl-link-color: #3777b0;
-$gl-dark-link-color: #333;
-$gl-placeholder-color: #8f8f8f;
-$gl-icon-color: $gl-placeholder-color;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
-$gl-gray-light: $gl-placeholder-color;
$gl-header-color: #4c4e54;
+$gl-header-nav-hover-color: #434343;
/*
* Lists
*/
$list-font-size: $gl-font-size;
-$list-title-color: $gl-title-color;
+$list-title-color: $gl-text-color;
$list-text-color: $gl-text-color;
+$list-text-disabled-color: $gl-text-color-disabled;
+$list-border-light: #eee;
+$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
+$list-warning-row-bg: #fcf8e3;
+$list-warning-row-border: #faebcc;
+$list-warning-row-color: #8a6d3b;
/*
* Markdown
*/
-$md-text-color: $gl-text-color;
$md-link-color: $gl-link-color;
+$md-area-border: #ddd;
/*
* Code
*/
-$code_font_size: 13px;
-$code_line_height: 1.5;
+$code_font_size: 12px;
+$code_line_height: 1.6;
/*
* Padding
@@ -149,22 +151,58 @@ $gl-sidebar-padding: 22px;
$row-hover: #f7faff;
$row-hover-border: #b2d7ff;
$progress-color: #c0392b;
-$avatar_radius: 50%;
$header-height: 50px;
$fixed-layout-width: 1280px;
+$limited-layout-width: 990px;
$gl-avatar-size: 40px;
$error-exclamation-point: #e62958;
$border-radius-default: 2px;
-$btn-transparent-color: #8f8f8f;
$settings-icon-size: 18px;
-$provider-btn-group-border: #e5e5e5;
$provider-btn-not-active-color: #4688f1;
$link-underline-blue: #4a8bee;
+$active-item-blue: #4a8bee;
$layout-link-gray: #7e7c7c;
-$todo-alert-blue: #428bca;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
+$issue-status-expired: #cea61b;
+$issuable-sidebar-color: $gl-text-color-secondary;
+$show-aside-bg: #eee;
+$show-aside-color: #777;
+$show-aside-shadow: #ddd;
+$group-path-color: #999;
+$namespace-kind-color: #aaa;
+$panel-heading-link-color: #777;
+$graph-author-email-color: #777;
+$count-arrow-border: #dce0e5;
+$save-project-loader-color: #555;
+$divergence-graph-bar-bg: #ccc;
+$divergence-graph-separator-bg: #ccc;
+$general-hover-transition-duration: 150ms;
+$general-hover-transition-curve: linear;
+
+
+/*
+* Common component specific colors
+*/
+$hint-color: #999;
+$well-pre-bg: #eee;
+$well-pre-color: #555;
+$loading-color: #555;
+$update-author-color: #999;
+$user-mention-color: #2fa0bb;
+$time-color: #999;
+$project-member-show-color: #aaa;
+$gl-promo-color: #aaa;
+$error-bg: #c67;
+$warning-message-bg: #ffffe6;
+$warning-message-border: #ed9;
+$warning-message-color: #b90;
+$control-group-descr-color: #666;
+$table-permission-x-bg: #d9edf7;
+$username-color: #666;
+$description-color: #666;
+$profiler-border: #eee;
/* tanuki logo colors */
$tanuki-red: #e24329;
@@ -195,101 +233,312 @@ $line-removed-dark: #fac5cd;
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
-$match-line: $gray-light;
-$table-border-gray: #f0f0f0;
$line-target-blue: #f6faff;
$line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
+$dark-diff-match-bg: rgba(255, 255, 255, 0.3);
+$dark-diff-match-color: rgba(255, 255, 255, 0.1);
+$file-mode-changed: #777;
+$file-mode-changed: #777;
+$diff-image-bg: #ddd;
+$diff-image-info-color: grey;
+$diff-swipe-border: #999;
+$diff-view-modes-color: grey;
+$diff-view-modes-border: #c1c1c1;
/*
* Fonts
*/
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif;
+$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
/*
* Dropdowns
*/
$dropdown-width: 300px;
-$dropdown-bg: #fff;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04);
-$dropdown-border-color: rgba(#000, .1);
+$dropdown-border-color: $border-color;
$dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
-$dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #555;
+$dropdown-input-fa-color: #c7c7c7;
$dropdown-input-focus-border: $focus-border-color;
$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6);
+$dropdown-chevron-size: 10px;
+$dropdown-toggle-active-border-color: darken($border-color, 14%);
+
-$dropdown-toggle-bg: #fff;
-$dropdown-toggle-color: #626262;
-$dropdown-toggle-border-color: #eaeaea;
-$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
-$dropdown-toggle-icon-color: #c4c4c4;
-$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
+/*
+* Filtered Search
+*/
+$dropdown-hover-color: #3b86ff;
/*
* Buttons
*/
$btn-active-gray: #ececec;
-$btn-placeholder-gray: #c7c7c7;
+$btn-active-gray-light: e4e7ed;
$btn-white-active: #848484;
-$btn-gray-hover: #eee;
+
+/*
+* Badges
+*/
+$badge-bg: #eee;
+$badge-color: $gl-text-color-secondary;
/*
* Award emoji
*/
-$award-emoji-menu-bg: #fff;
-$award-emoji-menu-border: #f1f2f4;
-$award-emoji-new-btn-icon-color: #dcdcdc;
+$award-emoji-menu-shadow: rgba(0,0,0,.175);
/*
* Search Box
*/
$search-input-border-color: rgba(#4688f1, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
-$search-input-width: 244px;
-$location-badge-color: #aaa;
-$location-badge-bg: $gray-normal;
+$search-input-width: 220px;
$location-badge-active-bg: #4f91f8;
$location-icon-color: #e7e9ed;
-$location-icon-active-color: #807e7e;
/*
* Notes
*/
-$notes-light-color: #8e8e8e;
-$notes-action-color: #c3c3c3;
-$notes-role-color: #8e8e8e;
-$notes-role-border-color: #e4e4e4;
-
+$notes-light-color: $gl-text-color-secondary;
+$notes-role-color: $gl-text-color-secondary;
$note-disabled-comment-color: #b2b2b2;
-$note-form-border-color: #e5e5e5;
-$note-toolbar-color: #959494;
+$note-targe3-outside: #fffff0;
+$note-targe3-inside: #ffffd3;
+$note-line2-border: #ddd;
-$zen-control-hover-color: #111;
-$calendar-header-color: #b8b8b8;
+/*
+* Zen
+*/
+$zen-control-color: #555;
+
+/*
+* Calendar
+*/
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
-$calendar-unselectable-bg: $gray-light;
+$calendar-user-contrib-text: #959494;
/*
* Cycle Analytics
*/
$cycle-analytics-box-padding: 30px;
$cycle-analytics-box-text-color: #8c8c8c;
+$cycle-analytics-big-font: 19px;
+$cycle-analytics-dark-text: $gl-text-color;
+$cycle-analytics-light-gray: #bfbfbf;
+$cycle-analytics-dismiss-icon-color: #b2b2b2;
/*
- * Personal Access Tokens
- */
-$personal-access-tokens-disabled-label-color: #bbb;
+* CI
+*/
+$ci-skipped-color: #888;
+
+/*
+* Boards
+*/
+$issue-boards-font-size: 14px;
+$issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
+
+/*
+* Avatar
+*/
+$avatar_radius: 50%;
+$avatar-border: rgba(0, 0, 0, .1);
+$gl-avatar-size: 40px;
+
+/*
+* Builds
+*/
+$builds-trace-bg: #111;
+
+/*
+* Callout
+*/
+$callout-danger-bg: #fdf7f7;
+$callout-danger-border: #eed3d7;
+$callout-danger-color: #b94a48;
+$callout-warning-bg: #faf8f0;
+$callout-warning-border: #faebcc;
+$callout-warning-color: #8a6d3b;
+$callout-info-bg: #f4f8fa;
+$callout-info-border: #bce8f1;
+$callout-info-color: #34789a;
+$callout-success-bg: #dff0d8;
+$callout-success-border: #5ca64d;
+$callout-success-color: #3c763d;
+
+/*
+* Commit Page
+*/
+$commit-max-width-marker-color: rgba(0, 0, 0, 0.0);
+$commit-message-text-area-bg: rgba(0, 0, 0, 0.0);
+
+/*
+* Common
+*/
+$common-gray: $gl-text-color;
+$common-gray-light: #bbb;
+$common-gray-dark: #444;
+$common-red: $gl-text-red;
+$common-green: $gl-text-green;
+
+/*
+* Editor
+*/
+$editor-cancel-color: #b94a48;
+
+/*
+* Events
+*/
+$events-pre-color: #777;
+$events-note-icon-color: #777;
+$events-body-border: #ddd;
+
+/*
+* Files
+*/
+$file-image-bg: #eee;
+$blob-bg: #eee;
+$blame-border: #eee;
+$blame-line-numbers-border: #ddd;
+$logs-bg: #eee;
+$logs-li-color: #888;
+$logs-p-color: #333;
-$ci-output-bg: #1d1f21;
-$ci-text-color: #c5c8c6;
+/*
+* Forms
+*/
+$input-danger-bg: #f2dede;
+$input-danger-border: #d66;
+$input-group-addon-bg: #f7f8fa;
+$gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
+$gl-field-focus-shadow-error: rgba(210, 40, 82, 0.6);
+
+/*
+* Help
+*/
+$document-index-color: #888;
+$help-shortcut-color: #999;
+$help-shortcut-mapping-color: #555;
+$help-shortcut-header-color: #333;
+
+/*
+* Issues
+*/
+$issues-today-bg: #f3fff2;
+$issues-today-border: #e1e8d5;
+$compare-display-color: #888;
+
+/*
+* jQuery UI
+*/
+$jq-ui-border: #ddd;
+$jq-ui-default-color: #777;
+
+/*
+* Label
+*/
+$label-gray-bg: #f8fafc;
+$label-inverse-bg: #333;
+$label-remove-border: rgba(0, 0, 0, .1);
+$label-border-radius: 100px;
+
+/*
+* Lint
+*/
+$lint-incorrect-color: red;
+$lint-correct-color: #47a447;
+
+/*
+* Login
+*/
+$login-brand-holder-color: #888;
+$login-devise-error-color: #a00;
+
+/*
+* Nav
+*/
+$nav-link-gray: #959494;
+$nav-badge-bg: #eee;
+$nav-toggle-gray: #666;
+
+/*
+* Notify
+*/
+$notify-details: #777;
+$notify-footer: #777;
+$notify-new-file: #090;
+$notify-deleted-file: #b00;
-$issue-boards-font-size: 15px;
+/*
+* Projects
+*/
+$project-option-descr-color: #54565b;
+$project-breadcrumb-color: #999;
+$project-private-forks-notice-odd: #2aa056;
+$project-network-controls-color: #888;
+
+/*
+* Runners
+*/
+$runner-state-shared-bg: #32b186;
+$runner-state-specific-bg: #3498db;
+$runner-status-online-color: $green-normal;
+$runner-status-offline-color: $gray-darkest;
+$runner-status-paused-color: $red-normal;
+
+/*
+Stat Graph
+*/
+$stat-graph-common-bg: #f3f3f3;
+$stat-graph-area-fill: #1db34f;
+$stat-graph-axis-fill: #aaa;
+$stat-graph-orange-fill: #f17f49;
+$stat-graph-selection-fill: #333;
+$stat-graph-selection-stroke: #333;
+
+/*
+* Selects
+*/
+$select2-drop-shadow1: rgba(76, 86, 103, 0.247059);
+$select2-drop-shadow2: rgba(31, 37, 50, 0.317647);
+
+
+/*
+* Todo
+*/
+$todo-alert-blue: #428bca;
+$todo-body-pre-color: #777;
+$todo-body-border: #ddd;
+
+/*
+* Typography
+*/
+$kdb-bg: #fcfcfc;
+$kdb-color: #555;
+$kdb-border: #ccc;
+$kdb-border-bottom: #bbb;
+$kdb-shadow: #bbb;
+$body-text-shadow: rgba(255,255,255,0.01);
+
+/*
+* UI Dev Kit
+*/
+$ui-dev-kit-example-color: #bbb;
+$ui-dev-kit-example-border: #ddd;
+
+/*
+Pipeline Graph
+*/
+$stage-hover-bg: #eaf3fc;
+$stage-hover-border: #d1e7fc;
+$action-icon-color: #d6d6d6; \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
new file mode 100644
index 00000000000..32eb750180f
--- /dev/null
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -0,0 +1,58 @@
+.info-well {
+ background: $gray-light;
+ color: $gl-text-color;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ .well-segment {
+ padding: $gl-padding;
+
+ &:not(:last-of-type) {
+ border-bottom: 1px solid $well-inner-border;
+ }
+
+ &.branch-info {
+ .monospace,
+ .commit-info {
+ margin-left: 4px;
+ }
+ }
+ }
+
+ .icon-container {
+ display: inline-block;
+ margin-right: 8px;
+
+ svg {
+ position: relative;
+ top: 2px;
+ height: 16px;
+ width: 16px;
+ }
+
+ &.commit-icon {
+ svg {
+ path {
+ fill: $gl-text-color;
+ }
+ }
+ }
+ }
+
+ .label.label-gray {
+ background-color: $well-expand-item;
+ }
+}
+
+.light-well {
+ background-color: $gray-light;
+ padding: 15px;
+}
+
+.well-centered {
+ h1 {
+ font-weight: normal;
+ text-align: center;
+ font-size: 48px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index ff02ebdd34c..97ade638db6 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -1,6 +1,6 @@
.zen-backdrop {
&.fullscreen {
- background-color: white;
+ background-color: $white-light;
position: fixed;
top: 0;
bottom: 0;
@@ -12,7 +12,7 @@
border: none;
box-shadow: none;
border-radius: 0;
- color: #000;
+ color: $black;
font-size: 20px;
line-height: 26px;
padding: 30px;
@@ -34,13 +34,13 @@
.zen-control {
padding: 0;
- color: #555;
+ color: $zen-control-color;
background: none;
border: 0;
}
.zen-control-full {
- color: $note-toolbar-color;
+ color: $gl-text-color-secondary;
&:hover {
color: $gl-link-color;
@@ -57,6 +57,6 @@
font-size: 36px;
&:hover {
- color: $zen-control-hover-color;
+ color: $black;
}
}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index d22d9b01495..cb923166b25 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -1,27 +1,109 @@
/* https://github.com/MozMorris/tomorrow-pygments */
+
+/*
+* Dark syntax colors
+*/
+$dark-new-bg: rgba(51, 255, 51, 0.1);
+$dark-new-idiff: rgba(51, 255, 51, 0.2);
+$dark-old-bg: rgba(255, 51, 51, 0.2);
+$dark-old-idiff: rgba(255, 51, 51, 0.25);
+$dark-border: #808080;
+$dark-code-border: #666;
+$dark-main-bg: #1d1f21;
+$dark-main-color: #1d1f21;
+$dark-line-color: #c5c8c6;
+$dark-line-num-color: rgba(255, 255, 255, 0.3);
+$dark-diff-not-empty-bg: #557;
+$dark-highlight-bg: #ffe792;
+$dark-highlight-color: $black;
+$dark-pre-hll-bg: #373b41;
+$dark-hll-bg: #373b41;
+$dark-c: #969896;
+$dark-err: #c66;
+$dark-k: #b294bb;
+$dark-l: #de935f;
+$dark-n: #c5c8c6;
+$dark-o: #8abeb7;
+$dark-p: #c5c8c6;
+$dark-cm: #969896;
+$dark-cp: #969896;
+$dark-c1: #969896;
+$dark-cs: #969896;
+$dark-gd: #c66;
+$dark-gh: #c5c8c6;
+$dark-gi: #b5bd68;
+$dark-gp: #969896;
+$dark-gu: #8abeb7;
+$dark-kc: #b294bb;
+$dark-kd: #b294bb;
+$dark-kn: #8abeb7;
+$dark-kp: #b294bb;
+$dark-kr: #b294bb;
+$dark-kt: #f0c674;
+$dark-ld: #b5bd68;
+$dark-m: #de935f;
+$dark-s: #b5bd68;
+$dark-na: #81a2be;
+$dark-nb: #c5c8c6;
+$dark-nc: #f0c674;
+$dark-no: #c66;
+$dark-nd: #8abeb7;
+$dark-ni: #c5c8c6;
+$dark-ne: #c66;
+$dark-nf: #81a2be;
+$dark-nl: #c5c8c6;
+$dark-nn: #f0c674;
+$dark-nx: #81a2be;
+$dark-py: #c5c8c6;
+$dark-nt: #8abeb7;
+$dark-nv: #c66;
+$dark-ow: #8abeb7;
+$dark-w: #c5c8c6;
+$dark-mf: #de935f;
+$dark-mh: #de935f;
+$dark-mi: #de935f;
+$dark-mo: #de935f;
+$dark-sb: #b5bd68;
+$dark-sc: #c5c8c6;
+$dark-sd: #969896;
+$dark-s2: #b5bd68;
+$dark-se: #de935f;
+$dark-sh: #b5bd68;
+$dark-si: #de935f;
+$dark-sx: #b5bd68;
+$dark-sr: #b5bd68;
+$dark-s1: #b5bd68;
+$dark-ss: #b5bd68;
+$dark-bp: #c5c8c6;
+$dark-vc: #c66;
+$dark-vg: #c66;
+$dark-vi: #c66;
+$dark-il: #de935f;
+
.code.dark {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: #1d1f21;
+ background-color: $dark-main-bg;
}
.diff-line-num,
.diff-line-num a {
- color: rgba(255, 255, 255, 0.3);
+ color: $dark-main-color;
+ color: $dark-line-num-color;
}
// Code itself
pre.code,
.diff-line-num {
- border-color: #666;
+ border-color: $dark-code-border;
}
&,
pre.code,
.line_holder .line_content {
- background-color: #1d1f21;
- color: #c5c8c6;
+ background-color: $dark-main-bg;
+ color: $dark-line-color;
}
// Diff line
@@ -32,18 +114,18 @@
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
- background-color: #557;
- border-color: darken(#557, 15%);
+ background-color: $dark-diff-not-empty-bg;
+ border-color: darken($dark-diff-not-empty-bg, 15%);
}
.diff-line-num.new,
.line_content.new {
- @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080);
+ @include diff_background($dark-new-bg, $dark-new-idiff, $dark-border);
}
.diff-line-num.old,
.line_content.old {
- @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.25), #808080);
+ @include diff_background($dark-old-bg, $dark-old-idiff, $dark-border);
}
.line_content.match {
@@ -53,77 +135,77 @@
// highlight line via anchor
pre .hll {
- background-color: #557 !important;
+ background-color: $dark-pre-hll-bg !important;
}
// Search result highlight
span.highlight_word {
- background-color: #ffe792 !important;
- color: #000 !important;
+ background-color: $dark-highlight-bg !important;
+ color: $dark-highlight-color !important;
}
- .hll { background-color: #373b41; }
- .c { color: #969896; } /* Comment */
- .err { color: #c66; } /* Error */
- .k { color: #b294bb; } /* Keyword */
- .l { color: #de935f; } /* Literal */
- .n { color: #c5c8c6; } /* Name */
- .o { color: #8abeb7; } /* Operator */
- .p { color: #c5c8c6; } /* Punctuation */
- .cm { color: #969896; } /* Comment.Multiline */
- .cp { color: #969896; } /* Comment.Preproc */
- .c1 { color: #969896; } /* Comment.Single */
- .cs { color: #969896; } /* Comment.Special */
- .gd { color: #c66; } /* Generic.Deleted */
+ .hll { background-color: $dark-hll-bg; }
+ .c { color: $dark-c; } /* Comment */
+ .err { color: $dark-err; } /* Error */
+ .k { color: $dark-k; } /* Keyword */
+ .l { color: $dark-l; } /* Literal */
+ .n { color: $dark-n; } /* Name */
+ .o { color: $dark-o; } /* Operator */
+ .p { color: $dark-p; } /* Punctuation */
+ .cm { color: $dark-cm; } /* Comment.Multiline */
+ .cp { color: $dark-cp; } /* Comment.Preproc */
+ .c1 { color: $dark-c1; } /* Comment.Single */
+ .cs { color: $dark-cs; } /* Comment.Special */
+ .gd { color: $dark-gd; } /* Generic.Deleted */
.ge { font-style: italic; } /* Generic.Emph */
- .gh { color: #c5c8c6; font-weight: bold; } /* Generic.Heading */
- .gi { color: #b5bd68; } /* Generic.Inserted */
- .gp { color: #969896; font-weight: bold; } /* Generic.Prompt */
+ .gh { color: $dark-gh; font-weight: bold; } /* Generic.Heading */
+ .gi { color: $dark-gi; } /* Generic.Inserted */
+ .gp { color: $dark-gp; font-weight: bold; } /* Generic.Prompt */
.gs { font-weight: bold; } /* Generic.Strong */
- .gu { color: #8abeb7; font-weight: bold; } /* Generic.Subheading */
- .kc { color: #b294bb; } /* Keyword.Constant */
- .kd { color: #b294bb; } /* Keyword.Declaration */
- .kn { color: #8abeb7; } /* Keyword.Namespace */
- .kp { color: #b294bb; } /* Keyword.Pseudo */
- .kr { color: #b294bb; } /* Keyword.Reserved */
- .kt { color: #f0c674; } /* Keyword.Type */
- .ld { color: #b5bd68; } /* Literal.Date */
- .m { color: #de935f; } /* Literal.Number */
- .s { color: #b5bd68; } /* Literal.String */
- .na { color: #81a2be; } /* Name.Attribute */
- .nb { color: #c5c8c6; } /* Name.Builtin */
- .nc { color: #f0c674; } /* Name.Class */
- .no { color: #c66; } /* Name.Constant */
- .nd { color: #8abeb7; } /* Name.Decorator */
- .ni { color: #c5c8c6; } /* Name.Entity */
- .ne { color: #c66; } /* Name.Exception */
- .nf { color: #81a2be; } /* Name.Function */
- .nl { color: #c5c8c6; } /* Name.Label */
- .nn { color: #f0c674; } /* Name.Namespace */
- .nx { color: #81a2be; } /* Name.Other */
- .py { color: #c5c8c6; } /* Name.Property */
- .nt { color: #8abeb7; } /* Name.Tag */
- .nv { color: #c66; } /* Name.Variable */
- .ow { color: #8abeb7; } /* Operator.Word */
- .w { color: #c5c8c6; } /* Text.Whitespace */
- .mf { color: #de935f; } /* Literal.Number.Float */
- .mh { color: #de935f; } /* Literal.Number.Hex */
- .mi { color: #de935f; } /* Literal.Number.Integer */
- .mo { color: #de935f; } /* Literal.Number.Oct */
- .sb { color: #b5bd68; } /* Literal.String.Backtick */
- .sc { color: #c5c8c6; } /* Literal.String.Char */
- .sd { color: #969896; } /* Literal.String.Doc */
- .s2 { color: #b5bd68; } /* Literal.String.Double */
- .se { color: #de935f; } /* Literal.String.Escape */
- .sh { color: #b5bd68; } /* Literal.String.Heredoc */
- .si { color: #de935f; } /* Literal.String.Interpol */
- .sx { color: #b5bd68; } /* Literal.String.Other */
- .sr { color: #b5bd68; } /* Literal.String.Regex */
- .s1 { color: #b5bd68; } /* Literal.String.Single */
- .ss { color: #b5bd68; } /* Literal.String.Symbol */
- .bp { color: #c5c8c6; } /* Name.Builtin.Pseudo */
- .vc { color: #c66; } /* Name.Variable.Class */
- .vg { color: #c66; } /* Name.Variable.Global */
- .vi { color: #c66; } /* Name.Variable.Instance */
- .il { color: #de935f; } /* Literal.Number.Integer.Long */
+ .gu { color: $dark-gu; font-weight: bold; } /* Generic.Subheading */
+ .kc { color: $dark-kc; } /* Keyword.Constant */
+ .kd { color: $dark-kd; } /* Keyword.Declaration */
+ .kn { color: $dark-kn; } /* Keyword.Namespace */
+ .kp { color: $dark-kp; } /* Keyword.Pseudo */
+ .kr { color: $dark-kr; } /* Keyword.Reserved */
+ .kt { color: $dark-kt; } /* Keyword.Type */
+ .ld { color: $dark-ld; } /* Literal.Date */
+ .m { color: $dark-m; } /* Literal.Number */
+ .s { color: $dark-s; } /* Literal.String */
+ .na { color: $dark-na; } /* Name.Attribute */
+ .nb { color: $dark-nb; } /* Name.Builtin */
+ .nc { color: $dark-nc; } /* Name.Class */
+ .no { color: $dark-no; } /* Name.Constant */
+ .nd { color: $dark-nd; } /* Name.Decorator */
+ .ni { color: $dark-ni; } /* Name.Entity */
+ .ne { color: $dark-ne; } /* Name.Exception */
+ .nf { color: $dark-nf; } /* Name.Function */
+ .nl { color: $dark-nl; } /* Name.Label */
+ .nn { color: $dark-nn; } /* Name.Namespace */
+ .nx { color: $dark-nx; } /* Name.Other */
+ .py { color: $dark-py; } /* Name.Property */
+ .nt { color: $dark-nt; } /* Name.Tag */
+ .nv { color: $dark-nv; } /* Name.Variable */
+ .ow { color: $dark-ow; } /* Operator.Word */
+ .w { color: $dark-w; } /* Text.Whitespace */
+ .mf { color: $dark-mf; } /* Literal.Number.Float */
+ .mh { color: $dark-mh; } /* Literal.Number.Hex */
+ .mi { color: $dark-mi; } /* Literal.Number.Integer */
+ .mo { color: $dark-mo; } /* Literal.Number.Oct */
+ .sb { color: $dark-sb; } /* Literal.String.Backtick */
+ .sc { color: $dark-sc; } /* Literal.String.Char */
+ .sd { color: $dark-sd; } /* Literal.String.Doc */
+ .s2 { color: $dark-s2; } /* Literal.String.Double */
+ .se { color: $dark-se; } /* Literal.String.Escape */
+ .sh { color: $dark-sh; } /* Literal.String.Heredoc */
+ .si { color: $dark-si; } /* Literal.String.Interpol */
+ .sx { color: $dark-sx; } /* Literal.String.Other */
+ .sr { color: $dark-sr; } /* Literal.String.Regex */
+ .s1 { color: $dark-s1; } /* Literal.String.Single */
+ .ss { color: $dark-ss; } /* Literal.String.Symbol */
+ .bp { color: $dark-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $dark-vc; } /* Name.Variable.Class */
+ .vg { color: $dark-vg; } /* Name.Variable.Global */
+ .vi { color: $dark-vi; } /* Name.Variable.Instance */
+ .il { color: $dark-il; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index db8da8aab10..d8510baad8a 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -1,27 +1,108 @@
/* https://github.com/richleland/pygments-css/blob/master/monokai.css */
+
+/*
+* Monokai Colors
+*/
+$monokai-bg: #272822;
+$monokai-border: #555;
+$monokai-text-color: #f8f8f2;
+$monokai-line-num-color: rgba(255, 255, 255, 0.3);
+$monokai-line-empty-bg: #49483e;
+$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
+$monokai-diff-border: #808080;
+$monokai-highlight-bg: #ffe792;
+
+$monokai-new-bg: rgba(166, 226, 46, 0.1);
+$monokai-new-idiff: rgba(166, 226, 46, 0.15);
+
+$monokai-old-bg: rgba(254, 147, 140, 0.15);
+$monokai-old-idiff: rgba(254, 147, 140, 0.2);
+
+$monokai-hll: #49483e;
+$monokai-c: #75715e;
+$monokai-err-color: #960050;
+$monokai-err-bg: #1e0010;
+$monokai-k: #66d9ef;
+$monokai-l: #ae81ff;
+$monokai-n: #f8f8f2;
+$monokai-o: #f92672;
+$monokai-p: #f8f8f2;
+$monokai-cm: #75715e;
+$monokai-cp: #75715e;
+$monokai-c1: #75715e;
+$monokai-cs: #75715e;
+$monokai-kc: #66d9ef;
+$monokai-kd: #66d9ef;
+$monokai-kn: #f92672;
+$monokai-kp: #66d9ef;
+$monokai-kr: #66d9ef;
+$monokai-kt: #66d9ef;
+$monokai-ld: #e6db74;
+$monokai-m: #ae81ff;
+$monokai-s: #e6db74;
+$monokai-na: #a6e22e;
+$monokai-nb: #f8f8f2;
+$monokai-nc: #a6e22e;
+$monokai-no: #66d9ef;
+$monokai-nd: #a6e22e;
+$monokai-ni: #f8f8f2;
+$monokai-ne: #a6e22e;
+$monokai-nf: #a6e22e;
+$monokai-nl: #f8f8f2;
+$monokai-nn: #f8f8f2;
+$monokai-nx: #a6e22e;
+$monokai-py: #f8f8f2;
+$monokai-nt: #f92672;
+$monokai-nv: #f8f8f2;
+$monokai-ow: #f92672;
+$monokai-w: #f8f8f2;
+$monokai-mf: #ae81ff;
+$monokai-mh: #ae81ff;
+$monokai-mi: #ae81ff;
+$monokai-mo: #ae81ff;
+$monokai-sb: #e6db74;
+$monokai-sc: #e6db74;
+$monokai-sd: #e6db74;
+$monokai-s2: #e6db74;
+$monokai-se: #ae81ff;
+$monokai-sh: #e6db74;
+$monokai-si: #e6db74;
+$monokai-sx: #e6db74;
+$monokai-sr: #e6db74;
+$monokai-s1: #e6db74;
+$monokai-ss: #e6db74;
+$monokai-bp: #f8f8f2;
+$monokai-vc: #f8f8f2;
+$monokai-vg: #f8f8f2;
+$monokai-vi: #f8f8f2;
+$monokai-il: #ae81ff;
+$monokai-gu: #75715e;
+$monokai-gd: #f92672;
+$monokai-gi: #a6e22e;
+
.code.monokai {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: #272822;
+ background-color: $monokai-bg;
}
.diff-line-num,
.diff-line-num a {
- color: rgba(255, 255, 255, 0.3);
+ color: $monokai-line-num-color;
}
// Code itself
pre.code,
.diff-line-num {
- border-color: #555;
+ border-color: $monokai-border;
}
&,
pre.code,
.line_holder .line_content {
- background-color: #272822;
- color: #f8f8f2;
+ background-color: $monokai-bg;
+ color: $monokai-text-color;
}
// Diff line
@@ -32,18 +113,18 @@
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
- background-color: #49483e;
- border-color: darken(#49483e, 15%);
+ background-color: $monokai-line-empty-bg;
+ border-color: $monokai-line-empty-border;
}
.diff-line-num.new,
.line_content.new {
- @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080);
+ @include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border);
}
.diff-line-num.old,
.line_content.old {
- @include diff_background(rgba(254, 147, 140, 0.15), rgba(254, 147, 140, 0.2), #808080);
+ @include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border);
}
.line_content.match {
@@ -53,75 +134,75 @@
// highlight line via anchor
pre .hll {
- background-color: #49483e !important;
+ background-color: $monokai-hll !important;
}
// Search result highlight
span.highlight_word {
- background-color: #ffe792 !important;
- color: #000 !important;
+ background-color: $monokai-highlight-bg !important;
+ color: $black !important;
}
- .hll { background-color: #49483e; }
- .c { color: #75715e; } /* Comment */
- .err { color: #960050; background-color: #1e0010; } /* Error */
- .k { color: #66d9ef; } /* Keyword */
- .l { color: #ae81ff; } /* Literal */
- .n { color: #f8f8f2; } /* Name */
- .o { color: #f92672; } /* Operator */
- .p { color: #f8f8f2; } /* Punctuation */
- .cm { color: #75715e; } /* Comment.Multiline */
- .cp { color: #75715e; } /* Comment.Preproc */
- .c1 { color: #75715e; } /* Comment.Single */
- .cs { color: #75715e; } /* Comment.Special */
+ .hll { background-color: $monokai-hll; }
+ .c { color: $monokai-c; } /* Comment */
+ .err { color: $monokai-err-color; background-color: $monokai-err-bg; } /* Error */
+ .k { color: $monokai-k; } /* Keyword */
+ .l { color: $monokai-l; } /* Literal */
+ .n { color: $monokai-n; } /* Name */
+ .o { color: $monokai-o; } /* Operator */
+ .p { color: $monokai-p; } /* Punctuation */
+ .cm { color: $monokai-cm; } /* Comment.Multiline */
+ .cp { color: $monokai-cp; } /* Comment.Preproc */
+ .c1 { color: $monokai-c1; } /* Comment.Single */
+ .cs { color: $monokai-cs; } /* Comment.Special */
.ge { font-style: italic; } /* Generic.Emph */
.gs { font-weight: bold; } /* Generic.Strong */
- .kc { color: #66d9ef; } /* Keyword.Constant */
- .kd { color: #66d9ef; } /* Keyword.Declaration */
- .kn { color: #f92672; } /* Keyword.Namespace */
- .kp { color: #66d9ef; } /* Keyword.Pseudo */
- .kr { color: #66d9ef; } /* Keyword.Reserved */
- .kt { color: #66d9ef; } /* Keyword.Type */
- .ld { color: #e6db74; } /* Literal.Date */
- .m { color: #ae81ff; } /* Literal.Number */
- .s { color: #e6db74; } /* Literal.String */
- .na { color: #a6e22e; } /* Name.Attribute */
- .nb { color: #f8f8f2; } /* Name.Builtin */
- .nc { color: #a6e22e; } /* Name.Class */
- .no { color: #66d9ef; } /* Name.Constant */
- .nd { color: #a6e22e; } /* Name.Decorator */
- .ni { color: #f8f8f2; } /* Name.Entity */
- .ne { color: #a6e22e; } /* Name.Exception */
- .nf { color: #a6e22e; } /* Name.Function */
- .nl { color: #f8f8f2; } /* Name.Label */
- .nn { color: #f8f8f2; } /* Name.Namespace */
- .nx { color: #a6e22e; } /* Name.Other */
- .py { color: #f8f8f2; } /* Name.Property */
- .nt { color: #f92672; } /* Name.Tag */
- .nv { color: #f8f8f2; } /* Name.Variable */
- .ow { color: #f92672; } /* Operator.Word */
- .w { color: #f8f8f2; } /* Text.Whitespace */
- .mf { color: #ae81ff; } /* Literal.Number.Float */
- .mh { color: #ae81ff; } /* Literal.Number.Hex */
- .mi { color: #ae81ff; } /* Literal.Number.Integer */
- .mo { color: #ae81ff; } /* Literal.Number.Oct */
- .sb { color: #e6db74; } /* Literal.String.Backtick */
- .sc { color: #e6db74; } /* Literal.String.Char */
- .sd { color: #e6db74; } /* Literal.String.Doc */
- .s2 { color: #e6db74; } /* Literal.String.Double */
- .se { color: #ae81ff; } /* Literal.String.Escape */
- .sh { color: #e6db74; } /* Literal.String.Heredoc */
- .si { color: #e6db74; } /* Literal.String.Interpol */
- .sx { color: #e6db74; } /* Literal.String.Other */
- .sr { color: #e6db74; } /* Literal.String.Regex */
- .s1 { color: #e6db74; } /* Literal.String.Single */
- .ss { color: #e6db74; } /* Literal.String.Symbol */
- .bp { color: #f8f8f2; } /* Name.Builtin.Pseudo */
- .vc { color: #f8f8f2; } /* Name.Variable.Class */
- .vg { color: #f8f8f2; } /* Name.Variable.Global */
- .vi { color: #f8f8f2; } /* Name.Variable.Instance */
- .il { color: #ae81ff; } /* Literal.Number.Integer.Long */
- .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */
- .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */
- .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */
+ .kc { color: $monokai-kc; } /* Keyword.Constant */
+ .kd { color: $monokai-kd; } /* Keyword.Declaration */
+ .kn { color: $monokai-kn; } /* Keyword.Namespace */
+ .kp { color: $monokai-kp; } /* Keyword.Pseudo */
+ .kr { color: $monokai-kr; } /* Keyword.Reserved */
+ .kt { color: $monokai-kt; } /* Keyword.Type */
+ .ld { color: $monokai-ld; } /* Literal.Date */
+ .m { color: $monokai-m; } /* Literal.Number */
+ .s { color: $monokai-s; } /* Literal.String */
+ .na { color: $monokai-na; } /* Name.Attribute */
+ .nb { color: $monokai-nb; } /* Name.Builtin */
+ .nc { color: $monokai-nc; } /* Name.Class */
+ .no { color: $monokai-no; } /* Name.Constant */
+ .nd { color: $monokai-nd; } /* Name.Decorator */
+ .ni { color: $monokai-ni; } /* Name.Entity */
+ .ne { color: $monokai-ne; } /* Name.Exception */
+ .nf { color: $monokai-nf; } /* Name.Function */
+ .nl { color: $monokai-nl; } /* Name.Label */
+ .nn { color: $monokai-nn; } /* Name.Namespace */
+ .nx { color: $monokai-nx; } /* Name.Other */
+ .py { color: $monokai-py; } /* Name.Property */
+ .nt { color: $monokai-nt; } /* Name.Tag */
+ .nv { color: $monokai-nv; } /* Name.Variable */
+ .ow { color: $monokai-ow; } /* Operator.Word */
+ .w { color: $monokai-w; } /* Text.Whitespace */
+ .mf { color: $monokai-mf; } /* Literal.Number.Float */
+ .mh { color: $monokai-mh; } /* Literal.Number.Hex */
+ .mi { color: $monokai-mi; } /* Literal.Number.Integer */
+ .mo { color: $monokai-mo; } /* Literal.Number.Oct */
+ .sb { color: $monokai-sb; } /* Literal.String.Backtick */
+ .sc { color: $monokai-sc; } /* Literal.String.Char */
+ .sd { color: $monokai-sd; } /* Literal.String.Doc */
+ .s2 { color: $monokai-s2; } /* Literal.String.Double */
+ .se { color: $monokai-se; } /* Literal.String.Escape */
+ .sh { color: $monokai-sh; } /* Literal.String.Heredoc */
+ .si { color: $monokai-si; } /* Literal.String.Interpol */
+ .sx { color: $monokai-sx; } /* Literal.String.Other */
+ .sr { color: $monokai-sr; } /* Literal.String.Regex */
+ .s1 { color: $monokai-s1; } /* Literal.String.Single */
+ .ss { color: $monokai-ss; } /* Literal.String.Symbol */
+ .bp { color: $monokai-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $monokai-vc; } /* Name.Variable.Class */
+ .vg { color: $monokai-vg; } /* Name.Variable.Global */
+ .vi { color: $monokai-vi; } /* Name.Variable.Instance */
+ .il { color: $monokai-il; } /* Literal.Number.Integer.Long */
+ .gu { color: $monokai-gu; } /* Generic.Subheading & Diff Unified/Comment? */
+ .gd { color: $monokai-gd; } /* Generic.Deleted & Diff Deleted */
+ .gi { color: $monokai-gi; } /* Generic.Inserted & Diff Inserted */
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index a87333146de..874aecb5e16 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -1,27 +1,112 @@
/* https://gist.github.com/qguv/7936275 */
+
+/*
+* Solarized dark colors
+*/
+$solarized-dark-new-bg: rgba(133, 153, 0, 0.15);
+$solarized-dark-new-idiff: rgba(133, 153, 0, 0.25);
+$solarized-dark-old-bg: rgba(220, 50, 47, 0.3);
+$solarized-dark-old-idiff: rgba(220, 50, 47, 0.25);
+$solarized-dark-border: #113b46;
+$solarized-dark-pre-bg: #002b36;
+$solarized-dark-pre-color: #93a1a1;
+$solarized-dark-pre-border: #113b46;
+$solarized-dark-line-bg: #002b36;
+$solarized-dark-line-color: rgba(255, 255, 255, 0.3);
+$solarized-dark-highlight: #094554;
+$solarized-dark-hll-bg: #174652;
+$solarized-dark-c: #586e75;
+$solarized-dark-err: #93a1a1;
+$solarized-dark-g: #93a1a1;
+$solarized-dark-k: #859900;
+$solarized-dark-l: #93a1a1;
+$solarized-dark-n: #93a1a1;
+$solarized-dark-o: #859900;
+$solarized-dark-x: #cb4b16;
+$solarized-dark-p: #93a1a1;
+$solarized-dark-cm: #586e75;
+$solarized-dark-cp: #859900;
+$solarized-dark-c1: #586e75;
+$solarized-dark-cs: #859900;
+$solarized-dark-gd: #2aa198;
+$solarized-dark-ge: #93a1a1;
+$solarized-dark-gr: #dc322f;
+$solarized-dark-gh: #cb4b16;
+$solarized-dark-gi: #859900;
+$solarized-dark-go: #93a1a1;
+$solarized-dark-gp: #93a1a1;
+$solarized-dark-gs: #93a1a1;
+$solarized-dark-gu: #cb4b16;
+$solarized-dark-gt: #93a1a1;
+$solarized-dark-kc: #cb4b16;
+$solarized-dark-kd: #268bd2;
+$solarized-dark-kn: #859900;
+$solarized-dark-kp: #859900;
+$solarized-dark-kr: #268bd2;
+$solarized-dark-kt: #dc322f;
+$solarized-dark-ld: #93a1a1;
+$solarized-dark-m: #2aa198;
+$solarized-dark-s: #2aa198;
+$solarized-dark-na: #93a1a1;
+$solarized-dark-nb: #b58900;
+$solarized-dark-nc: #268bd2;
+$solarized-dark-no: #cb4b16;
+$solarized-dark-nd: #268bd2;
+$solarized-dark-ni: #cb4b16;
+$solarized-dark-ne: #cb4b16;
+$solarized-dark-nf: #268bd2;
+$solarized-dark-nl: #93a1a1;
+$solarized-dark-nn: #93a1a1;
+$solarized-dark-nx: #93a1a1;
+$solarized-dark-py: #93a1a1;
+$solarized-dark-nt: #268bd2;
+$solarized-dark-nv: #268bd2;
+$solarized-dark-ow: #859900;
+$solarized-dark-w: #93a1a1;
+$solarized-dark-mf: #2aa198;
+$solarized-dark-mh: #2aa198;
+$solarized-dark-mi: #2aa198;
+$solarized-dark-mo: #2aa198;
+$solarized-dark-sb: #586e75;
+$solarized-dark-sc: #2aa198;
+$solarized-dark-sd: #93a1a1;
+$solarized-dark-s2: #2aa198;
+$solarized-dark-se: #cb4b16;
+$solarized-dark-sh: #93a1a1;
+$solarized-dark-si: #2aa198;
+$solarized-dark-sx: #2aa198;
+$solarized-dark-sr: #dc322f;
+$solarized-dark-s1: #2aa198;
+$solarized-dark-ss: #2aa198;
+$solarized-dark-bp: #268bd2;
+$solarized-dark-vc: #268bd2;
+$solarized-dark-vg: #268bd2;
+$solarized-dark-vi: #268bd2;
+$solarized-dark-il: #2aa198;
+
.code.solarized-dark {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: #002b36;
+ background-color: $solarized-dark-line-bg;
}
.diff-line-num,
.diff-line-num a {
- color: rgba(255, 255, 255, 0.3);
+ color: $solarized-dark-line-color;
}
// Code itself
pre.code,
.diff-line-num {
- border-color: #113b46;
+ border-color: $solarized-dark-pre-border;
}
&,
pre.code,
.line_holder .line_content {
- background-color: #002b36;
- color: #93a1a1;
+ background-color: $solarized-dark-pre-bg;
+ color: $solarized-dark-pre-color;
}
// Diff line
@@ -32,18 +117,18 @@
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
- background-color: #174652;
- border-color: darken(#174652, 15%);
+ background-color: $solarized-dark-hll-bg;
+ border-color: darken($solarized-dark-hll-bg, 15%);
}
.diff-line-num.new,
.line_content.new {
- @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46);
+ @include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border);
}
.diff-line-num.old,
.line_content.old {
- @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.25), #113b46);
+ @include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border);
}
.line_content.match {
@@ -53,12 +138,12 @@
// highlight line via anchor
pre .hll {
- background-color: #174652 !important;
+ background-color: $solarized-dark-hll-bg !important;
}
// Search result highlight
span.highlight_word {
- background-color: #094554 !important;
+ background-color: $solarized-dark-highlight !important;
}
/* Solarized Dark
@@ -79,72 +164,72 @@
green #859900 operators, other keywords
*/
- .c { color: #586e75; } /* Comment */
- .err { color: #93a1a1; } /* Error */
- .g { color: #93a1a1; } /* Generic */
- .k { color: #859900; } /* Keyword */
- .l { color: #93a1a1; } /* Literal */
- .n { color: #93a1a1; } /* Name */
- .o { color: #859900; } /* Operator */
- .x { color: #cb4b16; } /* Other */
- .p { color: #93a1a1; } /* Punctuation */
- .cm { color: #586e75; } /* Comment.Multiline */
- .cp { color: #859900; } /* Comment.Preproc */
- .c1 { color: #586e75; } /* Comment.Single */
- .cs { color: #859900; } /* Comment.Special */
- .gd { color: #2aa198; } /* Generic.Deleted */
- .ge { color: #93a1a1; font-style: italic; } /* Generic.Emph */
- .gr { color: #dc322f; } /* Generic.Error */
- .gh { color: #cb4b16; } /* Generic.Heading */
- .gi { color: #859900; } /* Generic.Inserted */
- .go { color: #93a1a1; } /* Generic.Output */
- .gp { color: #93a1a1; } /* Generic.Prompt */
- .gs { color: #93a1a1; font-weight: bold; } /* Generic.Strong */
- .gu { color: #cb4b16; } /* Generic.Subheading */
- .gt { color: #93a1a1; } /* Generic.Traceback */
- .kc { color: #cb4b16; } /* Keyword.Constant */
- .kd { color: #268bd2; } /* Keyword.Declaration */
- .kn { color: #859900; } /* Keyword.Namespace */
- .kp { color: #859900; } /* Keyword.Pseudo */
- .kr { color: #268bd2; } /* Keyword.Reserved */
- .kt { color: #dc322f; } /* Keyword.Type */
- .ld { color: #93a1a1; } /* Literal.Date */
- .m { color: #2aa198; } /* Literal.Number */
- .s { color: #2aa198; } /* Literal.String */
- .na { color: #93a1a1; } /* Name.Attribute */
- .nb { color: #b58900; } /* Name.Builtin */
- .nc { color: #268bd2; } /* Name.Class */
- .no { color: #cb4b16; } /* Name.Constant */
- .nd { color: #268bd2; } /* Name.Decorator */
- .ni { color: #cb4b16; } /* Name.Entity */
- .ne { color: #cb4b16; } /* Name.Exception */
- .nf { color: #268bd2; } /* Name.Function */
- .nl { color: #93a1a1; } /* Name.Label */
- .nn { color: #93a1a1; } /* Name.Namespace */
- .nx { color: #93a1a1; } /* Name.Other */
- .py { color: #93a1a1; } /* Name.Property */
- .nt { color: #268bd2; } /* Name.Tag */
- .nv { color: #268bd2; } /* Name.Variable */
- .ow { color: #859900; } /* Operator.Word */
- .w { color: #93a1a1; } /* Text.Whitespace */
- .mf { color: #2aa198; } /* Literal.Number.Float */
- .mh { color: #2aa198; } /* Literal.Number.Hex */
- .mi { color: #2aa198; } /* Literal.Number.Integer */
- .mo { color: #2aa198; } /* Literal.Number.Oct */
- .sb { color: #586e75; } /* Literal.String.Backtick */
- .sc { color: #2aa198; } /* Literal.String.Char */
- .sd { color: #93a1a1; } /* Literal.String.Doc */
- .s2 { color: #2aa198; } /* Literal.String.Double */
- .se { color: #cb4b16; } /* Literal.String.Escape */
- .sh { color: #93a1a1; } /* Literal.String.Heredoc */
- .si { color: #2aa198; } /* Literal.String.Interpol */
- .sx { color: #2aa198; } /* Literal.String.Other */
- .sr { color: #dc322f; } /* Literal.String.Regex */
- .s1 { color: #2aa198; } /* Literal.String.Single */
- .ss { color: #2aa198; } /* Literal.String.Symbol */
- .bp { color: #268bd2; } /* Name.Builtin.Pseudo */
- .vc { color: #268bd2; } /* Name.Variable.Class */
- .vg { color: #268bd2; } /* Name.Variable.Global */
- .vi { color: #268bd2; } /* Name.Variable.Instance */
- .il { color: #2aa198; } /* Literal.Number.Integer.Long */
+ .c { color: $solarized-dark-c; } /* Comment */
+ .err { color: $solarized-dark-err; } /* Error */
+ .g { color: $solarized-dark-g; } /* Generic */
+ .k { color: $solarized-dark-k; } /* Keyword */
+ .l { color: $solarized-dark-l; } /* Literal */
+ .n { color: $solarized-dark-n; } /* Name */
+ .o { color: $solarized-dark-o; } /* Operator */
+ .x { color: $solarized-dark-x; } /* Other */
+ .p { color: $solarized-dark-p; } /* Punctuation */
+ .cm { color: $solarized-dark-cm; } /* Comment.Multiline */
+ .cp { color: $solarized-dark-cp; } /* Comment.Preproc */
+ .c1 { color: $solarized-dark-c1; } /* Comment.Single */
+ .cs { color: $solarized-dark-cs; } /* Comment.Special */
+ .gd { color: $solarized-dark-gd; } /* Generic.Deleted */
+ .ge { color: $solarized-dark-ge; font-style: italic; } /* Generic.Emph */
+ .gr { color: $solarized-dark-gr; } /* Generic.Error */
+ .gh { color: $solarized-dark-gh; } /* Generic.Heading */
+ .gi { color: $solarized-dark-gi; } /* Generic.Inserted */
+ .go { color: $solarized-dark-go; } /* Generic.Output */
+ .gp { color: $solarized-dark-gp; } /* Generic.Prompt */
+ .gs { color: $solarized-dark-gs; font-weight: bold; } /* Generic.Strong */
+ .gu { color: $solarized-dark-gu; } /* Generic.Subheading */
+ .gt { color: $solarized-dark-gt; } /* Generic.Traceback */
+ .kc { color: $solarized-dark-kc; } /* Keyword.Constant */
+ .kd { color: $solarized-dark-kd; } /* Keyword.Declaration */
+ .kn { color: $solarized-dark-kn; } /* Keyword.Namespace */
+ .kp { color: $solarized-dark-kp; } /* Keyword.Pseudo */
+ .kr { color: $solarized-dark-kr; } /* Keyword.Reserved */
+ .kt { color: $solarized-dark-kt; } /* Keyword.Type */
+ .ld { color: $solarized-dark-ld; } /* Literal.Date */
+ .m { color: $solarized-dark-m; } /* Literal.Number */
+ .s { color: $solarized-dark-s; } /* Literal.String */
+ .na { color: $solarized-dark-na; } /* Name.Attribute */
+ .nb { color: $solarized-dark-nb; } /* Name.Builtin */
+ .nc { color: $solarized-dark-nc; } /* Name.Class */
+ .no { color: $solarized-dark-no; } /* Name.Constant */
+ .nd { color: $solarized-dark-nd; } /* Name.Decorator */
+ .ni { color: $solarized-dark-ni; } /* Name.Entity */
+ .ne { color: $solarized-dark-ne; } /* Name.Exception */
+ .nf { color: $solarized-dark-nf; } /* Name.Function */
+ .nl { color: $solarized-dark-nl; } /* Name.Label */
+ .nn { color: $solarized-dark-nn; } /* Name.Namespace */
+ .nx { color: $solarized-dark-nx; } /* Name.Other */
+ .py { color: $solarized-dark-py; } /* Name.Property */
+ .nt { color: $solarized-dark-nt; } /* Name.Tag */
+ .nv { color: $solarized-dark-nv; } /* Name.Variable */
+ .ow { color: $solarized-dark-ow; } /* Operator.Word */
+ .w { color: $solarized-dark-w; } /* Text.Whitespace */
+ .mf { color: $solarized-dark-mf; } /* Literal.Number.Float */
+ .mh { color: $solarized-dark-mh; } /* Literal.Number.Hex */
+ .mi { color: $solarized-dark-mi; } /* Literal.Number.Integer */
+ .mo { color: $solarized-dark-mo; } /* Literal.Number.Oct */
+ .sb { color: $solarized-dark-sb; } /* Literal.String.Backtick */
+ .sc { color: $solarized-dark-sc; } /* Literal.String.Char */
+ .sd { color: $solarized-dark-sd; } /* Literal.String.Doc */
+ .s2 { color: $solarized-dark-s2; } /* Literal.String.Double */
+ .se { color: $solarized-dark-se; } /* Literal.String.Escape */
+ .sh { color: $solarized-dark-sh; } /* Literal.String.Heredoc */
+ .si { color: $solarized-dark-si; } /* Literal.String.Interpol */
+ .sx { color: $solarized-dark-sx; } /* Literal.String.Other */
+ .sr { color: $solarized-dark-sr; } /* Literal.String.Regex */
+ .s1 { color: $solarized-dark-s1; } /* Literal.String.Single */
+ .ss { color: $solarized-dark-ss; } /* Literal.String.Symbol */
+ .bp { color: $solarized-dark-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $solarized-dark-vc; } /* Name.Variable.Class */
+ .vg { color: $solarized-dark-vg; } /* Name.Variable.Global */
+ .vi { color: $solarized-dark-vi; } /* Name.Variable.Instance */
+ .il { color: $solarized-dark-il; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index faff353ded7..499a1c108b8 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,15 +1,99 @@
/* https://gist.github.com/qguv/7936275 */
+/*
+* Solarized light syntax colors
+*/
+$solarized-light-matchline-bg: rgba(255, 255, 255, 0.4);
+$solarized-light-new-bg: rgba(133, 153, 0, 0.2);
+$solarized-light-new-idiff: rgba(133, 153, 0, 0.25);
+$solarized-light-old-bg: rgba(220, 50, 47, 0.2);
+$solarized-light-old-idiff: rgba(220, 50, 47, 0.25);
+$solarized-light-border: #c5d0d4;
+$solarized-light-pre-bg: #002b36;
+$solarized-light-pre-bg: #fdf6e3;
+$solarized-light-pre-color: #586e75;
+$solarized-light-line-bg: #fdf6e3;
+$solarized-light-highlight: #eee8d5;
+$solarized-light-hll-bg: #ddd8c5;
+$solarized-light-c: #93a1a1;
+$solarized-light-err: #586e75;
+$solarized-light-g: #586e75;
+$solarized-light-k: #859900;
+$solarized-light-l: #586e75;
+$solarized-light-n: #586e75;
+$solarized-light-o: #859900;
+$solarized-light-x: #cb4b16;
+$solarized-light-p: #586e75;
+$solarized-light-cm: #93a1a1;
+$solarized-light-cp: #859900;
+$solarized-light-c1: #93a1a1;
+$solarized-light-cs: #859900;
+$solarized-light-gd: #2aa198;
+$solarized-light-ge: #586e75;
+$solarized-light-gr: #dc322f;
+$solarized-light-gh: #cb4b16;
+$solarized-light-gi: #859900;
+$solarized-light-go: #586e75;
+$solarized-light-gp: #586e75;
+$solarized-light-gs: #586e75;
+$solarized-light-gu: #cb4b16;
+$solarized-light-gt: #586e75;
+$solarized-light-kc: #cb4b16;
+$solarized-light-kd: #268bd2;
+$solarized-light-kn: #859900;
+$solarized-light-kp: #859900;
+$solarized-light-kr: #268bd2;
+$solarized-light-kt: #dc322f;
+$solarized-light-ld: #586e75;
+$solarized-light-m: #2aa198;
+$solarized-light-s: #2aa198;
+$solarized-light-na: #586e75;
+$solarized-light-nb: #b58900;
+$solarized-light-nc: #268bd2;
+$solarized-light-no: #cb4b16;
+$solarized-light-nd: #268bd2;
+$solarized-light-ni: #cb4b16;
+$solarized-light-ne: #cb4b16;
+$solarized-light-nf: #268bd2;
+$solarized-light-nl: #586e75;
+$solarized-light-nn: #586e75;
+$solarized-light-nx: #586e75;
+$solarized-light-py: #586e75;
+$solarized-light-nt: #268bd2;
+$solarized-light-nv: #268bd2;
+$solarized-light-ow: #859900;
+$solarized-light-w: #586e75;
+$solarized-light-mf: #2aa198;
+$solarized-light-mh: #2aa198;
+$solarized-light-mi: #2aa198;
+$solarized-light-mo: #2aa198;
+$solarized-light-sb: #93a1a1;
+$solarized-light-sc: #2aa198;
+$solarized-light-sd: #586e75;
+$solarized-light-s2: #2aa198;
+$solarized-light-se: #cb4b16;
+$solarized-light-sh: #586e75;
+$solarized-light-si: #2aa198;
+$solarized-light-sx: #2aa198;
+$solarized-light-sr: #dc322f;
+$solarized-light-s1: #2aa198;
+$solarized-light-ss: #2aa198;
+$solarized-light-bp: #268bd2;
+$solarized-light-vc: #268bd2;
+$solarized-light-vg: #268bd2;
+$solarized-light-vi: #268bd2;
+$solarized-light-il: #2aa198;
+
@mixin matchLine {
color: $black-transparent;
- background: rgba(255, 255, 255, 0.4);
+ background: $solarized-light-matchline-bg;
}
.code.solarized-light {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: #fdf6e3;
+ background-color: $solarized-light-line-bg;
}
.diff-line-num,
@@ -20,14 +104,14 @@
// Code itself
pre.code,
.diff-line-num {
- border-color: #c5d0d4;
+ border-color: $solarized-light-border;
}
&,
pre.code,
.line_holder .line_content {
- background-color: #fdf6e3;
- color: #586e75;
+ background-color: $solarized-light-pre-bg;
+ color: $solarized-light-pre-color;
}
// Diff line
@@ -38,18 +122,19 @@
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
- background-color: #ddd8c5;
- border-color: darken(#ddd8c5, 15%);
+ background-color: $solarized-light-hll-bg;
+ border-color: darken($solarized-light-hll-bg, 15%);
}
.diff-line-num.new,
.line_content.new {
- @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4);
+ @include diff_background($solarized-light-new-bg,
+ $solarized-light-new-idiff, $solarized-light-border);
}
.diff-line-num.old,
.line_content.old {
- @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.25), #c5d0d4);
+ @include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border);
}
.line_content.match {
@@ -59,12 +144,12 @@
// highlight line via anchor
pre .hll {
- background-color: #ddd8c5 !important;
+ background-color: $solarized-light-hll-bg !important;
}
// Search result highlight
span.highlight_word {
- background-color: #eee8d5 !important;
+ background-color: $solarized-light-highlight !important;
}
/* Solarized Light
@@ -85,72 +170,72 @@
green #859900 operators, other keywords
*/
- .c { color: #93a1a1; } /* Comment */
- .err { color: #586e75; } /* Error */
- .g { color: #586e75; } /* Generic */
- .k { color: #859900; } /* Keyword */
- .l { color: #586e75; } /* Literal */
- .n { color: #586e75; } /* Name */
- .o { color: #859900; } /* Operator */
- .x { color: #cb4b16; } /* Other */
- .p { color: #586e75; } /* Punctuation */
- .cm { color: #93a1a1; } /* Comment.Multiline */
- .cp { color: #859900; } /* Comment.Preproc */
- .c1 { color: #93a1a1; } /* Comment.Single */
- .cs { color: #859900; } /* Comment.Special */
- .gd { color: #2aa198; } /* Generic.Deleted */
- .ge { color: #586e75; font-style: italic; } /* Generic.Emph */
- .gr { color: #dc322f; } /* Generic.Error */
- .gh { color: #cb4b16; } /* Generic.Heading */
- .gi { color: #859900; } /* Generic.Inserted */
- .go { color: #586e75; } /* Generic.Output */
- .gp { color: #586e75; } /* Generic.Prompt */
- .gs { color: #586e75; font-weight: bold; } /* Generic.Strong */
- .gu { color: #cb4b16; } /* Generic.Subheading */
- .gt { color: #586e75; } /* Generic.Traceback */
- .kc { color: #cb4b16; } /* Keyword.Constant */
- .kd { color: #268bd2; } /* Keyword.Declaration */
- .kn { color: #859900; } /* Keyword.Namespace */
- .kp { color: #859900; } /* Keyword.Pseudo */
- .kr { color: #268bd2; } /* Keyword.Reserved */
- .kt { color: #dc322f; } /* Keyword.Type */
- .ld { color: #586e75; } /* Literal.Date */
- .m { color: #2aa198; } /* Literal.Number */
- .s { color: #2aa198; } /* Literal.String */
- .na { color: #586e75; } /* Name.Attribute */
- .nb { color: #b58900; } /* Name.Builtin */
- .nc { color: #268bd2; } /* Name.Class */
- .no { color: #cb4b16; } /* Name.Constant */
- .nd { color: #268bd2; } /* Name.Decorator */
- .ni { color: #cb4b16; } /* Name.Entity */
- .ne { color: #cb4b16; } /* Name.Exception */
- .nf { color: #268bd2; } /* Name.Function */
- .nl { color: #586e75; } /* Name.Label */
- .nn { color: #586e75; } /* Name.Namespace */
- .nx { color: #586e75; } /* Name.Other */
- .py { color: #586e75; } /* Name.Property */
- .nt { color: #268bd2; } /* Name.Tag */
- .nv { color: #268bd2; } /* Name.Variable */
- .ow { color: #859900; } /* Operator.Word */
- .w { color: #586e75; } /* Text.Whitespace */
- .mf { color: #2aa198; } /* Literal.Number.Float */
- .mh { color: #2aa198; } /* Literal.Number.Hex */
- .mi { color: #2aa198; } /* Literal.Number.Integer */
- .mo { color: #2aa198; } /* Literal.Number.Oct */
- .sb { color: #93a1a1; } /* Literal.String.Backtick */
- .sc { color: #2aa198; } /* Literal.String.Char */
- .sd { color: #586e75; } /* Literal.String.Doc */
- .s2 { color: #2aa198; } /* Literal.String.Double */
- .se { color: #cb4b16; } /* Literal.String.Escape */
- .sh { color: #586e75; } /* Literal.String.Heredoc */
- .si { color: #2aa198; } /* Literal.String.Interpol */
- .sx { color: #2aa198; } /* Literal.String.Other */
- .sr { color: #dc322f; } /* Literal.String.Regex */
- .s1 { color: #2aa198; } /* Literal.String.Single */
- .ss { color: #2aa198; } /* Literal.String.Symbol */
- .bp { color: #268bd2; } /* Name.Builtin.Pseudo */
- .vc { color: #268bd2; } /* Name.Variable.Class */
- .vg { color: #268bd2; } /* Name.Variable.Global */
- .vi { color: #268bd2; } /* Name.Variable.Instance */
- .il { color: #2aa198; } /* Literal.Number.Integer.Long */
+ .c { color: $solarized-light-c; } /* Comment */
+ .err { color: $solarized-light-err; } /* Error */
+ .g { color: $solarized-light-g; } /* Generic */
+ .k { color: $solarized-light-k; } /* Keyword */
+ .l { color: $solarized-light-l; } /* Literal */
+ .n { color: $solarized-light-n; } /* Name */
+ .o { color: $solarized-light-o; } /* Operator */
+ .x { color: $solarized-light-x; } /* Other */
+ .p { color: $solarized-light-p; } /* Punctuation */
+ .cm { color: $solarized-light-cm; } /* Comment.Multiline */
+ .cp { color: $solarized-light-cp; } /* Comment.Preproc */
+ .c1 { color: $solarized-light-c1; } /* Comment.Single */
+ .cs { color: $solarized-light-cs; } /* Comment.Special */
+ .gd { color: $solarized-light-gd; } /* Generic.Deleted */
+ .ge { color: $solarized-light-ge; font-style: italic; } /* Generic.Emph */
+ .gr { color: $solarized-light-gr; } /* Generic.Error */
+ .gh { color: $solarized-light-gh; } /* Generic.Heading */
+ .gi { color: $solarized-light-gi; } /* Generic.Inserted */
+ .go { color: $solarized-light-go; } /* Generic.Output */
+ .gp { color: $solarized-light-gp; } /* Generic.Prompt */
+ .gs { color: $solarized-light-gs; font-weight: bold; } /* Generic.Strong */
+ .gu { color: $solarized-light-gu; } /* Generic.Subheading */
+ .gt { color: $solarized-light-gt; } /* Generic.Traceback */
+ .kc { color: $solarized-light-kc; } /* Keyword.Constant */
+ .kd { color: $solarized-light-kd; } /* Keyword.Declaration */
+ .kn { color: $solarized-light-kn; } /* Keyword.Namespace */
+ .kp { color: $solarized-light-kp; } /* Keyword.Pseudo */
+ .kr { color: $solarized-light-kr; } /* Keyword.Reserved */
+ .kt { color: $solarized-light-kt; } /* Keyword.Type */
+ .ld { color: $solarized-light-ld; } /* Literal.Date */
+ .m { color: $solarized-light-m; } /* Literal.Number */
+ .s { color: $solarized-light-s; } /* Literal.String */
+ .na { color: $solarized-light-na; } /* Name.Attribute */
+ .nb { color: $solarized-light-nb; } /* Name.Builtin */
+ .nc { color: $solarized-light-nc; } /* Name.Class */
+ .no { color: $solarized-light-no; } /* Name.Constant */
+ .nd { color: $solarized-light-nd; } /* Name.Decorator */
+ .ni { color: $solarized-light-ni; } /* Name.Entity */
+ .ne { color: $solarized-light-ne; } /* Name.Exception */
+ .nf { color: $solarized-light-nf; } /* Name.Function */
+ .nl { color: $solarized-light-nl; } /* Name.Label */
+ .nn { color: $solarized-light-nn; } /* Name.Namespace */
+ .nx { color: $solarized-light-nx; } /* Name.Other */
+ .py { color: $solarized-light-py; } /* Name.Property */
+ .nt { color: $solarized-light-nt; } /* Name.Tag */
+ .nv { color: $solarized-light-nv; } /* Name.Variable */
+ .ow { color: $solarized-light-ow; } /* Operator.Word */
+ .w { color: $solarized-light-w; } /* Text.Whitespace */
+ .mf { color: $solarized-light-mf; } /* Literal.Number.Float */
+ .mh { color: $solarized-light-mh; } /* Literal.Number.Hex */
+ .mi { color: $solarized-light-mi; } /* Literal.Number.Integer */
+ .mo { color: $solarized-light-mo; } /* Literal.Number.Oct */
+ .sb { color: $solarized-light-sb; } /* Literal.String.Backtick */
+ .sc { color: $solarized-light-sc; } /* Literal.String.Char */
+ .sd { color: $solarized-light-sd; } /* Literal.String.Doc */
+ .s2 { color: $solarized-light-s2; } /* Literal.String.Double */
+ .se { color: $solarized-light-se; } /* Literal.String.Escape */
+ .sh { color: $solarized-light-sh; } /* Literal.String.Heredoc */
+ .si { color: $solarized-light-si; } /* Literal.String.Interpol */
+ .sx { color: $solarized-light-sx; } /* Literal.String.Other */
+ .sr { color: $solarized-light-sr; } /* Literal.String.Regex */
+ .s1 { color: $solarized-light-s1; } /* Literal.String.Single */
+ .ss { color: $solarized-light-ss; } /* Literal.String.Symbol */
+ .bp { color: $solarized-light-bp; } /* Name.Builtin.Pseudo */
+ .vc { color: $solarized-light-vc; } /* Name.Variable.Class */
+ .vg { color: $solarized-light-vg; } /* Name.Variable.Global */
+ .vi { color: $solarized-light-vi; } /* Name.Variable.Instance */
+ .il { color: $solarized-light-il; } /* Literal.Number.Integer.Long */
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index d5367d5f3f0..b425c78e0d5 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,15 +1,82 @@
/* https://github.com/aahan/pygments-github-style */
+/*
+* White Syntax Colors
+*/
+$white-code-color: $gl-text-color;
+$white-highlight: #fafe3d;
+$white-pre-hll-bg: #f8eec7;
+$white-hll-bg: #f8f8f8;
+$white-c: #998;
+$white-err: #a61717;
+$white-err-bg: #e3d2d2;
+$white-cm: #998;
+$white-cp: #999;
+$white-c1: #998;
+$white-cs: #999;
+$white-gd: $black;
+$white-gd-bg: #fdd;
+$white-gd-x: $black;
+$white-gd-x-bg: #faa;
+$white-gr: #a00;
+$white-gh: #999;
+$white-gi: $black;
+$white-gi-bg: #dfd;
+$white-gi-x: $black;
+$white-gi-x-bg: #afa;
+$white-go: #888;
+$white-gp: #555;
+$white-gu: #800080;
+$white-gt: #a00;
+$white-kt: #458;
+$white-m: #099;
+$white-s: #d14;
+$white-n: #333;
+$white-na: teal;
+$white-nb: #0086b3;
+$white-nc: #458;
+$white-no: teal;
+$white-ni: purple;
+$white-ne: #900;
+$white-nf: #900;
+$white-nn: #555;
+$white-nt: navy;
+$white-nv: teal;
+$white-w: #bbb;
+$white-mf: #099;
+$white-mh: #099;
+$white-mi: #099;
+$white-mo: #099;
+$white-sb: #d14;
+$white-sc: #d14;
+$white-sd: #d14;
+$white-s2: #d14;
+$white-se: #d14;
+$white-sh: #d14;
+$white-si: #d14;
+$white-sx: #d14;
+$white-sr: #009926;
+$white-s1: #d14;
+$white-ss: #990073;
+$white-bp: #999;
+$white-vc: teal;
+$white-vg: teal;
+$white-vi: teal;
+$white-il: #099;
+$white-gc-color: #999;
+$white-gc-bg: #eaf2f5;
+
+
@mixin matchLine {
color: $black-transparent;
- background-color: $match-line;
+ background-color: $gray-light;
}
.code.white {
// Line numbers
.line-numbers,
.diff-line-num {
- background-color: $background-color;
+ background-color: $gray-light;
}
.diff-line-num,
@@ -20,14 +87,14 @@
// Code itself
pre.code,
.diff-line-num {
- border-color: $table-border-gray;
+ border-color: $white-normal;
}
&,
pre.code,
.line_holder .line_content {
- background-color: #fff;
- color: #333;
+ background-color: $white-light;
+ color: $white-code-color;
}
// Diff line
@@ -83,75 +150,75 @@
// highlight line via anchor
pre .hll {
- background-color: #f8eec7 !important;
+ background-color: $white-pre-hll-bg !important;
}
// Search result highlight
span.highlight_word {
- background-color: #fafe3d !important;
+ background-color: $white-highlight !important;
}
- .hll { background-color: #f8f8f8; }
- .c { color: #998; font-style: italic; }
- .err { color: #a61717; background-color: #e3d2d2; }
+ .hll { background-color: $white-hll-bg; }
+ .c { color: $white-c; font-style: italic; }
+ .err { color: $white-err; background-color: $white-err-bg; }
.k { font-weight: bold; }
.o { font-weight: bold; }
- .cm { color: #998; font-style: italic; }
- .cp { color: #999; font-weight: bold; }
- .c1 { color: #998; font-style: italic; }
- .cs { color: #999; font-weight: bold; font-style: italic; }
- .gd { color: #000; background-color: #fdd; }
- .gd .x { color: #000; background-color: #faa; }
+ .cm { color: $white-cm; font-style: italic; }
+ .cp { color: $white-cp; font-weight: bold; }
+ .c1 { color: $white-c1; font-style: italic; }
+ .cs { color: $white-cs; font-weight: bold; font-style: italic; }
+ .gd { color: $white-gd; background-color: $white-gd-bg; }
+ .gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; }
.ge { font-style: italic; }
- .gr { color: #a00; }
- .gh { color: #999; }
- .gi { color: #000; background-color: #dfd; }
- .gi .x { color: #000; background-color: #afa; }
- .go { color: #888; }
- .gp { color: #555; }
+ .gr { color: $white-gr; }
+ .gh { color: $white-gh; }
+ .gi { color: $white-gi; background-color: $white-gi-bg; }
+ .gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; }
+ .go { color: $white-go; }
+ .gp { color: $white-gp; }
.gs { font-weight: bold; }
- .gu { color: #800080; font-weight: bold; }
- .gt { color: #a00; }
+ .gu { color: $white-gu; font-weight: bold; }
+ .gt { color: $white-gt; }
.kc { font-weight: bold; }
.kd { font-weight: bold; }
.kn { font-weight: bold; }
.kp { font-weight: bold; }
.kr { font-weight: bold; }
- .kt { color: #458; font-weight: bold; }
- .m { color: #099; }
- .s { color: #d14; }
- .n { color: #333; }
- .na { color: teal; }
- .nb { color: #0086b3; }
- .nc { color: #458; font-weight: bold; }
- .no { color: teal; }
- .ni { color: purple; }
- .ne { color: #900; font-weight: bold; }
- .nf { color: #900; font-weight: bold; }
- .nn { color: #555; }
- .nt { color: navy; }
- .nv { color: teal; }
+ .kt { color: $white-kt; font-weight: bold; }
+ .m { color: $white-m; }
+ .s { color: $white-s; }
+ .n { color: $white-n; }
+ .na { color: $white-na; }
+ .nb { color: $white-nb; }
+ .nc { color: $white-nc; font-weight: bold; }
+ .no { color: $white-no; }
+ .ni { color: $white-ni; }
+ .ne { color: $white-ne; font-weight: bold; }
+ .nf { color: $white-nf; font-weight: bold; }
+ .nn { color: $white-nn; }
+ .nt { color: $white-nt; }
+ .nv { color: $white-nv; }
.ow { font-weight: bold; }
- .w { color: #bbb; }
- .mf { color: #099; }
- .mh { color: #099; }
- .mi { color: #099; }
- .mo { color: #099; }
- .sb { color: #d14; }
- .sc { color: #d14; }
- .sd { color: #d14; }
- .s2 { color: #d14; }
- .se { color: #d14; }
- .sh { color: #d14; }
- .si { color: #d14; }
- .sx { color: #d14; }
- .sr { color: #009926; }
- .s1 { color: #d14; }
- .ss { color: #990073; }
- .bp { color: #999; }
- .vc { color: teal; }
- .vg { color: teal; }
- .vi { color: teal; }
- .il { color: #099; }
- .gc { color: #999; background-color: #eaf2f5; }
+ .w { color: $white-w; }
+ .mf { color: $white-mf; }
+ .mh { color: $white-mh; }
+ .mi { color: $white-mi; }
+ .mo { color: $white-mo; }
+ .sb { color: $white-sb; }
+ .sc { color: $white-sc; }
+ .sd { color: $white-sd; }
+ .s2 { color: $white-s2; }
+ .se { color: $white-se; }
+ .sh { color: $white-sh; }
+ .si { color: $white-si; }
+ .sx { color: $white-sx; }
+ .sr { color: $white-sr; }
+ .s1 { color: $white-s1; }
+ .ss { color: $white-ss; }
+ .bp { color: $white-bp; }
+ .vc { color: $white-vc; }
+ .vg { color: $white-vg; }
+ .vi { color: $white-vi; }
+ .il { color: $white-il; }
+ .gc { color: $white-gc-color; background-color: $white-gc-bg; }
}
diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss
index b2bce482fde..9f613710cf4 100644
--- a/app/assets/stylesheets/mailers/devise.scss
+++ b/app/assets/stylesheets/mailers/devise.scss
@@ -1,3 +1,5 @@
+@import "framework/variables";
+
// NOTE: This stylesheet is for the exclusive use of the `devise_mailer` layout
// used for Devise email templates, and _should not_ be included in any
// application stylesheets.
@@ -46,7 +48,7 @@ table {
&#body {
background-color: $message-background-color;
- border: 1px solid #000;
+ border: 1px solid $black;
border-radius: 4px;
margin: 0 auto;
width: 600px;
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
new file mode 100644
index 00000000000..60ff72c703e
--- /dev/null
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -0,0 +1,207 @@
+@import "framework/variables";
+
+// This file is largely copied from `highlight/white.scss`, but modified to
+// avoid all descendant selectors (`table td`). This is because the CSS inlining
+// we use performs dramatically worse on descendant selectors than the
+// alternatives.
+// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632>
+//
+// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of
+// preference): plain class selectors, type (element name) selectors, or
+// explicit child selectors.
+
+/*
+* Highlighted Diff Email Syntax Colors
+*/
+$highlighted-highlight-word: #fafe3d;
+$highlighted-hll-bg: #f8f8f8;
+$highlighted-c: #998;
+$highlighted-err: #a61717;
+$highlighted-err-bg: #e3d2d2;
+$highlighted-cm: #998;
+$highlighted-cp: #999;
+$highlighted-c1: #998;
+$highlighted-cs: #999;
+$highlighted-gd: #000;
+$highlighted-gd-bg: #fdd;
+$highlighted-gd-x: #000;
+$highlighted-gd-x-bg: #faa;
+$highlighted-gr: #a00;
+$highlighted-gh: #999;
+$highlighted-gi: #000;
+$highlighted-gi-bg: #dfd;
+$highlighted-gi-x: #000;
+$highlighted-gi-x-bg: #afa;
+$highlighted-go: #888;
+$highlighted-gp: #555;
+$highlighted-gu: #800080;
+$highlighted-gt: #a00;
+$highlighted-kt: #458;
+$highlighted-m: #099;
+$highlighted-s: #d14;
+$highlighted-n: #333;
+$highlighted-na: teal;
+$highlighted-nb: #0086b3;
+$highlighted-nc: #458;
+$highlighted-no: teal;
+$highlighted-ni: purple;
+$highlighted-ne: #900;
+$highlighted-nf: #900;
+$highlighted-nn: #555;
+$highlighted-nt: navy;
+$highlighted-nv: teal;
+$highlighted-w: #bbb;
+$highlighted-mf: #099;
+$highlighted-mh: #099;
+$highlighted-mi: #099;
+$highlighted-mo: #099;
+$highlighted-sb: #d14;
+$highlighted-sc: #d14;
+$highlighted-sd: #d14;
+$highlighted-s2: #d14;
+$highlighted-se: #d14;
+$highlighted-sh: #d14;
+$highlighted-si: #d14;
+$highlighted-sx: #d14;
+$highlighted-sr: #009926;
+$highlighted-s1: #d14;
+$highlighted-ss: #990073;
+$highlighted-bp: #999;
+$highlighted-vc: teal;
+$highlighted-vg: teal;
+$highlighted-vi: teal;
+$highlighted-il: #099;
+$highlighted-gc: #999;
+$highlighted-gc-bg: #eaf2f5;
+
+.code {
+ background-color: $white-light;
+ font-family: monospace;
+ font-size: $code_font_size;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+
+ > tr {
+ line-height: $code_line_height;
+ }
+}
+
+.diff-line-num {
+ padding: 0 5px;
+ text-align: right;
+ width: 35px;
+ background-color: $gray-light;
+ color: $black-transparent;
+ border-right: 1px solid $white-normal;
+
+ &.old {
+ background-color: $line-number-old;
+ border-right-color: $line-removed-dark;
+ }
+
+ &.new {
+ background-color: $line-number-new;
+ border-right-color: $line-added-dark;
+ }
+}
+
+.line_content {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+
+ &.old {
+ background-color: $line-removed;
+
+ > .line > span.idiff,
+ > .line > span > span.idiff {
+ background-color: $line-removed-dark;
+ }
+ }
+
+ &.new {
+ background-color: $line-added;
+
+ > .line > span.idiff,
+ > .line > span > span.idiff {
+ background-color: $line-added-dark;
+ }
+ }
+
+ &.match {
+ color: $black-transparent;
+ background-color: $gray-light;
+ }
+}
+
+pre {
+ margin: 0;
+}
+
+span.highlight_word {
+ background-color: $highlighted-highlight-word !important;
+}
+
+.hll { background-color: $highlighted-hll-bg; }
+.c { color: $highlighted-c; font-style: italic; }
+.err { color: $highlighted-err; background-color: $highlighted-err-bg; }
+.k { font-weight: bold; }
+.o { font-weight: bold; }
+.cm { color: $highlighted-cm; font-style: italic; }
+.cp { color: $highlighted-cp; font-weight: bold; }
+.c1 { color: $highlighted-c1; font-style: italic; }
+.cs { color: $highlighted-cs; font-weight: bold; font-style: italic; }
+.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; }
+.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; }
+.ge { font-style: italic; }
+.gr { color: $highlighted-gr; }
+.gh { color: $highlighted-gh; }
+.gi { color: $highlighted-gi; background-color: $highlighted-gi-bg; }
+.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; }
+.go { color: $highlighted-go; }
+.gp { color: $highlighted-gp; }
+.gs { font-weight: bold; }
+.gu { color: $highlighted-gu; font-weight: bold; }
+.gt { color: $highlighted-gt; }
+.kc { font-weight: bold; }
+.kd { font-weight: bold; }
+.kn { font-weight: bold; }
+.kp { font-weight: bold; }
+.kr { font-weight: bold; }
+.kt { color: $highlighted-kt; font-weight: bold; }
+.m { color: $highlighted-m; }
+.s { color: $highlighted-s; }
+.n { color: $highlighted-n; }
+.na { color: $highlighted-na; }
+.nb { color: $highlighted-nb; }
+.nc { color: $highlighted-nc; font-weight: bold; }
+.no { color: $highlighted-no; }
+.ni { color: $highlighted-ni; }
+.ne { color: $highlighted-ne; font-weight: bold; }
+.nf { color: $highlighted-nf; font-weight: bold; }
+.nn { color: $highlighted-nn; }
+.nt { color: $highlighted-nt; }
+.nv { color: $highlighted-nv; }
+.ow { font-weight: bold; }
+.w { color: $highlighted-w; }
+.mf { color: $highlighted-mf; }
+.mh { color: $highlighted-mh; }
+.mi { color: $highlighted-mi; }
+.mo { color: $highlighted-mo; }
+.sb { color: $highlighted-sb; }
+.sc { color: $highlighted-sc; }
+.sd { color: $highlighted-sd; }
+.s2 { color: $highlighted-s2; }
+.se { color: $highlighted-se; }
+.sh { color: $highlighted-sh; }
+.si { color: $highlighted-si; }
+.sx { color: $highlighted-sx; }
+.sr { color: $highlighted-sr; }
+.s1 { color: $highlighted-s1; }
+.ss { color: $highlighted-ss; }
+.bp { color: $highlighted-bp; }
+.vc { color: $highlighted-vc; }
+.vg { color: $highlighted-vg; }
+.vi { color: $highlighted-vi; }
+.il { color: $highlighted-il; }
+.gc { color: $highlighted-gc; background-color: $highlighted-gc-bg; }
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
deleted file mode 100644
index 8d1a6020ca4..00000000000
--- a/app/assets/stylesheets/mailers/repository_push_email.scss
+++ /dev/null
@@ -1,143 +0,0 @@
-@import "framework/variables";
-
-// This file is largely copied from `highlight/white.scss`, but modified to
-// avoid all descendant selectors (`table td`). This is because the CSS inlining
-// we use performs dramatically worse on descendant selectors than the
-// alternatives.
-// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632>
-//
-// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of
-// preference): plain class selectors, type (element name) selectors, or
-// explicit child selectors.
-
-.code {
- background-color: #fff;
- font-family: monospace;
- font-size: $code_font_size;
- -premailer-cellpadding: 0;
- -premailer-cellspacing: 0;
- -premailer-width: 100%;
-
- > tr {
- line-height: $code_line_height;
- }
-}
-
-.diff-line-num {
- padding: 0 5px;
- text-align: right;
- width: 35px;
- background-color: $background-color;
- color: $black-transparent;
- border-right: 1px solid $table-border-gray;
-
- &.old {
- background-color: $line-number-old;
- border-right-color: $line-removed-dark;
- }
-
- &.new {
- background-color: $line-number-new;
- border-right-color: $line-added-dark;
- }
-}
-
-.line_content {
- padding-left: 0.5em;
- padding-right: 0.5em;
-
- &.old {
- background-color: $line-removed;
-
- > .line > span.idiff,
- > .line > span > span.idiff {
- background-color: $line-removed-dark;
- }
- }
-
- &.new {
- background-color: $line-added;
-
- > .line > span.idiff,
- > .line > span > span.idiff {
- background-color: $line-added-dark;
- }
- }
-
- &.match {
- color: $black-transparent;
- background-color: $match-line;
- }
-}
-
-pre {
- margin: 0;
-}
-
-span.highlight_word {
- background-color: #fafe3d !important;
-}
-
-.hll { background-color: #f8f8f8; }
-.c { color: #998; font-style: italic; }
-.err { color: #a61717; background-color: #e3d2d2; }
-.k { font-weight: bold; }
-.o { font-weight: bold; }
-.cm { color: #998; font-style: italic; }
-.cp { color: #999; font-weight: bold; }
-.c1 { color: #998; font-style: italic; }
-.cs { color: #999; font-weight: bold; font-style: italic; }
-.gd { color: #000; background-color: #fdd; }
-.gd .x { color: #000; background-color: #faa; }
-.ge { font-style: italic; }
-.gr { color: #a00; }
-.gh { color: #999; }
-.gi { color: #000; background-color: #dfd; }
-.gi .x { color: #000; background-color: #afa; }
-.go { color: #888; }
-.gp { color: #555; }
-.gs { font-weight: bold; }
-.gu { color: #800080; font-weight: bold; }
-.gt { color: #a00; }
-.kc { font-weight: bold; }
-.kd { font-weight: bold; }
-.kn { font-weight: bold; }
-.kp { font-weight: bold; }
-.kr { font-weight: bold; }
-.kt { color: #458; font-weight: bold; }
-.m { color: #099; }
-.s { color: #d14; }
-.n { color: #333; }
-.na { color: teal; }
-.nb { color: #0086b3; }
-.nc { color: #458; font-weight: bold; }
-.no { color: teal; }
-.ni { color: purple; }
-.ne { color: #900; font-weight: bold; }
-.nf { color: #900; font-weight: bold; }
-.nn { color: #555; }
-.nt { color: navy; }
-.nv { color: teal; }
-.ow { font-weight: bold; }
-.w { color: #bbb; }
-.mf { color: #099; }
-.mh { color: #099; }
-.mi { color: #099; }
-.mo { color: #099; }
-.sb { color: #d14; }
-.sc { color: #d14; }
-.sd { color: #d14; }
-.s2 { color: #d14; }
-.se { color: #d14; }
-.sh { color: #d14; }
-.si { color: #d14; }
-.sx { color: #d14; }
-.sr { color: #009926; }
-.s1 { color: #d14; }
-.ss { color: #990073; }
-.bp { color: #999; }
-.vc { color: teal; }
-.vg { color: teal; }
-.vi { color: teal; }
-.il { color: #099; }
-.gc { color: #999; background-color: #eaf2f5; }
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index ced8c4a9907..a81e5eb5ebf 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -1,3 +1,5 @@
+@import "framework/variables";
+
img {
max-width: 100%;
height: auto;
@@ -5,26 +7,14 @@ img {
p.details {
font-style: italic;
- color: #777;
+ color: $notify-details;
}
.footer > p {
font-size: small;
- color: #777;
+ color: $notify-footer;
}
pre.commit-message {
white-space: pre-wrap;
}
-
-.file-stats > a {
- text-decoration: none;
-
- > .new-file {
- color: #090;
- }
-
- > .deleted-file {
- color: #b00;
- }
-}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
deleted file mode 100644
index 6cefafd8fc7..00000000000
--- a/app/assets/stylesheets/pages/admin.scss
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * Admin area
- *
- */
-.admin-dashboard {
- .data {
- a {
- h1 {
- line-height: 48px;
- font-size: 48px;
- padding: 20px;
- text-align: center;
- font-weight: normal;
- }
- }
- }
-
- .str-truncated {
- max-width: 60%;
- }
-}
-
-.admin-filter form {
- .select2-container {
- width: 100%;
- }
-
- .controls {
- margin-left: 130px;
- }
-
- .form-actions {
- padding-left: 130px;
- background: #fff;
- }
-
- .visibility-levels {
- .controls {
- margin-bottom: 9px;
- }
-
- i {
- color: inherit;
- }
- }
-}
-
-.broadcast-messages {
- .message {
- line-height: 2;
- }
-}
-
-.broadcast-message {
- @extend .alert-warning;
- padding: 10px;
- text-align: center;
-
- > div,
- p {
- display: inline;
- margin: 0;
-
- a {
- color: inherit;
- text-decoration: underline;
- }
- }
-}
-
-.broadcast-message-preview {
- @extend .broadcast-message;
- margin-bottom: 20px;
-}
-
-// Users List
-
-.users-list {
- .user-row {
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- white-space: nowrap;
- }
-
- .user-details {
- flex: 1 1 auto;
- overflow: hidden;
- padding-right: 8px;
- }
-
- .user-name {
- display: inline-block;
- font-weight: 600;
- }
-
- .user-name,
- .user-email {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .dropdown {
- .btn-block {
- margin-bottom: 0;
- line-height: inherit;
- }
- }
-
- .label-default {
- color: $btn-transparent-color;
- }
-}
-
-.abuse-reports {
- .table {
- table-layout: fixed;
- }
-
- .subheading {
- padding-bottom: $gl-padding;
- }
-
- .message {
- word-wrap: break-word;
- }
-
- .btn {
- white-space: normal;
- padding: $gl-btn-padding;
- }
-
- th {
- width: 15%;
-
- &.wide {
- width: 55%;
- }
- }
-
- @media (max-width: $screen-sm-max) {
- th {
- width: 100%;
- }
-
- td {
- width: 100%;
- float: left;
- }
- }
-
- .no-reports {
- .emoji-icon {
- margin-left: $btn-side-margin;
- margin-top: 3px;
- }
-
- span {
- font-size: 19px;
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 47a7e84b5c6..f2d60bff2b5 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -31,7 +31,7 @@
.dropdown-content {
max-height: 150px;
- }
+ }
}
.issue-board-dropdown-content {
@@ -74,6 +74,7 @@
height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px);
min-height: 475px;
+ transition: width .2s;
&.is-compact {
width: calc(100% - 290px);
@@ -98,7 +99,7 @@
.board-inner {
height: 100%;
font-size: $issue-boards-font-size;
- background: $background-color;
+ background: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
@@ -109,6 +110,12 @@
&.has-border {
border-top: 3px solid;
+ margin-top: -1px;
+ margin-right: -1px;
+ margin-left: -1px;
+ padding-top: 1px;
+ padding-right: 1px;
+ padding-left: 1px;
.board-title {
padding-top: ($gl-padding - 3px);
@@ -145,7 +152,7 @@
.board-blank-state {
height: calc(100% - 49px);
padding: $gl-padding;
- background-color: #fff;
+ background-color: $white-light;
}
.board-blank-state-list {
@@ -166,8 +173,12 @@
}
}
-.board-list {
+.board-list-component {
height: calc(100% - 49px);
+}
+
+.board-list {
+ height: 100%;
margin-bottom: 0;
padding: 5px;
list-style: none;
@@ -175,7 +186,7 @@
overflow-x: hidden;
&.is-smaller {
- height: calc(100% - 185px);
+ height: calc(100% - 136px);
}
}
@@ -187,9 +198,9 @@
.card {
position: relative;
padding: 10px $gl-padding;
- background: #fff;
+ background: $white-light;
border-radius: $border-radius-default;
- box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
+ box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
&:not(:last-child) {
@@ -239,7 +250,7 @@
}
.issue-boards-search {
- width: 335px;
+ width: 290px;
.form-control {
display: inline-block;
@@ -249,7 +260,7 @@
.board-list-count {
padding: 10px 0;
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
font-size: 13px;
> .fa {
@@ -321,7 +332,6 @@
}
.issuable-header-text {
- width: 100%;
padding-right: 35px;
> strong {
@@ -329,3 +339,18 @@
}
}
}
+
+.right-sidebar.right-sidebar-expanded {
+ &.boards-sidebar-slide-enter-active,
+ &.boards-sidebar-slide-leave-active {
+ transition: width .2s,
+ padding .2s;
+ }
+
+ &.boards-sidebar-slide-enter,
+ &.boards-sidebar-slide-leave-active {
+ width: 0;
+ padding-left: 0;
+ padding-right: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss
new file mode 100644
index 00000000000..3e2fa8ca88d
--- /dev/null
+++ b/app/assets/stylesheets/pages/branches.scss
@@ -0,0 +1,55 @@
+.divergence-graph {
+ padding: 12px 12px 0 0;
+ float: right;
+
+ .graph-side {
+ position: relative;
+ width: 80px;
+ height: 22px;
+ padding: 5px 0 13px;
+ float: left;
+
+ .bar {
+ position: absolute;
+ height: 4px;
+ background-color: $divergence-graph-bar-bg;
+ }
+
+ .bar-behind {
+ right: 0;
+ border-radius: 3px 0 0 3px;
+ }
+
+ .bar-ahead {
+ left: 0;
+ border-radius: 0 3px 3px 0;
+ }
+
+ .count {
+ padding-top: 6px;
+ padding-bottom: 0;
+ font-size: 12px;
+ color: $gl-text-color;
+ display: block;
+ }
+
+ .count-behind {
+ padding-right: 4px;
+ text-align: right;
+ }
+
+ .count-ahead {
+ padding-left: 4px;
+ text-align: left;
+ }
+ }
+
+ .graph-separator {
+ position: relative;
+ width: 1px;
+ height: 18px;
+ margin: 5px 0 0;
+ float: left;
+ background-color: $divergence-graph-separator-bg;
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index f1d311cabbe..fd101d43b5b 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,7 +1,38 @@
+@keyframes fade-out-status {
+ 0%, 50% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+@keyframes blinking-dots {
+ 0% {
+ background-color: rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+
+ 25% {
+ background-color: rgba($white-light, 0.4);
+ box-shadow: 12px 0 0 0 rgba($white-light,2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+
+ 75% {
+ background-color: rgba($white-light, 0.4);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,1);
+ }
+
+ 100% {
+ background-color: rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+}
+
.build-page {
pre.trace {
- background: #111;
- color: #fff;
+ background: $builds-trace-bg;
+ color: $white-light;
font-family: $monospace_font;
white-space: pre-wrap;
overflow: auto;
@@ -14,32 +45,99 @@
}
}
- .scroll-controls {
- .scroll-step {
- width: 31px;
- margin: 0 0 0 auto;
+ .environment-information {
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ padding: 12px $gl-padding;
+ border-radius: $border-radius-default;
+
+ svg {
+ position: relative;
+ top: 1px;
+ margin-right: 5px;
}
+ }
+}
+
+.scroll-controls {
+ height: 100%;
+
+ .scroll-step {
+ width: 31px;
+ margin: 0 0 0 auto;
+ }
+
+ .scroll-link,
+ .autoscroll-container {
+ right: 25px;
+ z-index: 1;
+ }
- &.affix-bottom {
- position: absolute;
- right: 25px;
+ .scroll-link {
+ position: fixed;
+ display: block;
+ margin-bottom: 10px;
+
+ &.scroll-top .gitlab-icon-scroll-up-hover,
+ &.scroll-top:hover .gitlab-icon-scroll-up,
+ &.scroll-bottom .gitlab-icon-scroll-down-hover,
+ &.scroll-bottom:hover .gitlab-icon-scroll-down {
+ display: none;
}
- &.affix {
- right: 25px;
- bottom: 15px;
- z-index: 1;
+ &.scroll-top:hover .gitlab-icon-scroll-up-hover,
+ &.scroll-bottom:hover .gitlab-icon-scroll-down-hover {
+ display: inline-block;
}
- &.sidebar-expanded {
- right: #{$gutter_width + ($gl-padding * 2)};
+ &.scroll-top {
+ top: 110px;
}
- a {
- display: block;
- margin-bottom: 10px;
+ &.scroll-bottom {
+ bottom: -2px;
}
}
+
+ .autoscroll-container {
+ position: absolute;
+ }
+
+ &.sidebar-expanded {
+
+ .scroll-link,
+ .autoscroll-container {
+ right: ($gutter_width + ($gl-padding * 2));
+ }
+ }
+}
+
+.status-message {
+ display: inline-block;
+ color: $white-light;
+
+ .status-icon {
+ display: inline-block;
+ width: 16px;
+ height: 33px;
+ }
+
+ .status-text {
+ float: left;
+ opacity: 0;
+ margin-right: 10px;
+ font-weight: normal;
+ line-height: 1.8;
+ transition: opacity 1s ease-out;
+
+ &.animate {
+ animation: fade-out-status 2s ease;
+ }
+ }
+
+ &:hover .status-text {
+ opacity: 1;
+ }
}
.build-header {
@@ -49,12 +147,9 @@
min-height: 58px;
align-items: center;
- .btn-inverted {
- @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
- }
-
@media (max-width: $screen-sm-max) {
padding-right: 40px;
+ margin-top: 6px;
.btn-inverted {
display: none;
@@ -63,14 +158,14 @@
.header-content {
flex: 1;
- }
- a {
- color: $gl-gray;
+ a {
+ color: $gl-text-color;
- &:hover {
- color: $gl-link-color;
- text-decoration: none;
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
}
}
@@ -86,8 +181,8 @@
}
.build-trace {
- background: $ci-output-bg;
- color: $ci-text-color;
+ background: $black;
+ color: $gray-darkest;
white-space: pre;
overflow-x: auto;
font-size: 12px;
@@ -99,6 +194,15 @@
.bash {
display: block;
}
+
+ .build-loader-animation {
+ position: relative;
+ width: 6px;
+ height: 6px;
+ margin: auto auto 12px 2px;
+ border-radius: 50%;
+ animation: blinking-dots 1s linear infinite;
+ }
}
.right-sidebar.build-sidebar {
@@ -238,6 +342,12 @@
}
}
+.build-sidebar {
+ .container-fluid.container-limited {
+ max-width: 100%;
+ }
+}
+
.build-detail-row {
margin-bottom: 5px;
@@ -247,7 +357,7 @@
}
.build-light-text {
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
}
.build-gutter-toggle {
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index 87c453a7a27..90643832390 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -1,7 +1,7 @@
.ci-body {
.project-title {
margin: 0;
- color: #444;
+ color: $common-gray-dark;
font-size: 20px;
line-height: 1.5;
}
@@ -18,7 +18,7 @@
}
td {
- color: $gl-gray;
+ color: $gl-text-color;
vertical-align: middle !important;
a {
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
deleted file mode 100644
index 47d3e72679b..00000000000
--- a/app/assets/stylesheets/pages/commit.scss
+++ /dev/null
@@ -1,252 +0,0 @@
-.commit-title {
- display: block;
-}
-
-.commit-author,
-.commit-committer {
- display: block;
- color: #999;
- font-weight: normal;
- font-style: italic;
-}
-
-.commit-author strong,
-.commit-committer strong {
- font-weight: bold;
- font-style: normal;
-}
-
-.commit-description {
- background: none;
- border: none;
- margin: 0;
- padding: 0;
- margin-top: 10px;
- word-break: normal;
- white-space: pre-wrap;
-}
-
-.commit-info-row {
- margin-bottom: 10px;
- line-height: 24px;
- padding-top: 6px;
-
- &.commit-info-row-header {
- line-height: 34px;
- padding: 10px 0;
- margin-bottom: 0;
-
- @media (min-width: $screen-sm-min) {
- display: flex;
- align-items: center;
-
- .commit-meta {
- flex: 1;
- }
- }
-
- .commit-hash-full {
- @media (max-width: $screen-sm-max) {
- width: 80px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: inline-block;
- vertical-align: bottom;
- }
- }
-
- .commit-action-buttons {
- i {
- color: $gl-icon-color;
- font-size: 13px;
- margin-right: 3px;
- }
-
- @media (max-width: $screen-xs-max) {
- .dropdown {
- width: 100%;
- margin-top: 10px;
- }
-
- .dropdown-toggle {
- width: 100%;
- }
- }
- }
- }
-
- .avatar {
- @extend .avatar-inline;
- margin-left: 0;
-
- @media (min-width: $screen-sm-min) {
- margin-left: 4px;
- }
- }
-
- .commit-committer-link,
- .commit-author-link {
- color: $gl-gray;
- font-weight: bold;
- }
-
- .fa-clipboard {
- color: $dropdown-title-btn-color;
- }
-
- .commit-info {
- &.branches {
- margin-left: 8px;
- }
- }
-
- .ci-status-link {
-
- svg {
- position: relative;
- top: 2px;
- margin: 0 2px 0 3px;
- }
- }
-}
-
-.js-details-expand {
- &:hover {
- text-decoration: none;
- }
-}
-
-.commit-info-widget {
- background: $background-color;
- color: $gl-gray;
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
-
- .widget-row {
- padding: $gl-padding;
-
- &:not(:last-of-type) {
- border-bottom: 1px solid $widget-inner-border;
- }
-
- &.branch-info {
- .monospace,
- .commit-info {
- margin-left: 4px;
- }
- }
- }
-
- .icon-container {
- display: inline-block;
- margin-right: 8px;
-
- svg {
- position: relative;
- top: 2px;
- height: 16px;
- width: 16px;
- }
-
- &.commit-icon {
- svg {
- path {
- fill: $gl-text-color;
- }
- }
- }
- }
-
- .label.label-gray {
- background-color: $widget-expand-item;
- }
-}
-
-.ci-status-link {
- svg {
- overflow: visible;
- }
-}
-
-.commit-box {
- border-top: 1px solid $border-color;
- padding: $gl-padding 0;
-
- .commit-title {
- margin: 0;
- font-size: 23px;
- color: $gl-gray-dark;
- }
-
- .commit-description {
- margin-top: 15px;
- }
-}
-
-.file-stats {
- ul {
- list-style: none;
- margin: 0;
- padding: 10px 0;
-
- li {
- padding: 3px 0;
- line-height: 20px;
- }
- }
-
- .new-file {
- a {
- color: $gl-text-green;
- }
- }
-
- .renamed-file {
- a {
- color: $gl-text-orange;
- }
- }
-
- .deleted-file {
- a {
- color: $gl-text-red;
- }
- }
-
- .edit-file {
- a {
- color: $gl-text-color;
- }
- }
-}
-
-/*
- * Commit message textarea for web editor and
- * custom merge request message
- */
-.commit-message-container {
- background-color: $body-bg;
- position: relative;
- font-family: $monospace_font;
- $left: 12px;
- overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
- .max-width-marker {
- width: 72ch;
- color: rgba(0, 0, 0, 0.0);
- font-family: inherit;
- left: $left;
- height: 100%;
- border-right: 1px solid mix($input-border, white);
- position: absolute;
- z-index: 1;
- }
-
- > textarea {
- background-color: rgba(0, 0, 0, 0.0);
- font-family: inherit;
- padding-left: $left;
- position: relative;
- z-index: 2;
- }
-}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 98a84351a3d..fef8e8eec27 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -1,15 +1,83 @@
+.commit-description {
+ background: none;
+ border: none;
+ padding: 0;
+ margin-top: 10px;
+ word-break: normal;
+ white-space: pre-wrap;
+}
+
+.js-details-expand {
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.commit-box {
+ border-top: 1px solid $border-color;
+ padding: $gl-padding 0;
+
+ .commit-title {
+ margin: 0;
+ color: $gl-text-color;
+ }
+
+ .commit-description {
+ margin-top: 15px;
+ }
+}
+
+.commit-hash-full {
+ @media (max-width: $screen-sm-max) {
+ width: 80px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ vertical-align: bottom;
+ }
+}
+
+/*
+ * Commit message textarea for web editor and
+ * custom merge request message
+ */
+.commit-message-container {
+ background-color: $body-bg;
+ position: relative;
+ font-family: $monospace_font;
+ $left: 12px;
+ overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
+ .max-width-marker {
+ width: 72ch;
+ color: $commit-max-width-marker-color;
+ font-family: inherit;
+ left: $left;
+ height: 100%;
+ border-right: 1px solid mix($input-border, $white-light);
+ position: absolute;
+ z-index: 1;
+ }
+
+ textarea {
+ background-color: $commit-message-text-area-bg;
+ font-family: inherit;
+ padding-left: $left;
+ position: relative;
+ z-index: 2;
+ }
+}
+
+
.commits-compare-switch {
- @include btn-default;
- @include btn-white;
float: left;
margin-right: 9px;
}
.commit-header {
padding: 5px 10px;
- background-color: $background-color;
- border-top: 1px solid #eee;
- border-bottom: 1px solid #eee;
+ background-color: $gray-light;
+ border-bottom: 1px solid $gray-darker;
font-size: 14px;
&:first-child {
@@ -18,8 +86,6 @@
}
.commit-row-title {
- line-height: 1.35;
-
.notes_count {
float: right;
margin-right: 10px;
@@ -30,15 +96,14 @@
}
.commit-row-message {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
-
}
.text-expander {
display: inline-block;
background: $gray-light;
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
padding: 0 5px;
cursor: pointer;
border: 1px solid $border-gray-dark;
@@ -54,14 +119,15 @@
.commit-actions {
@media (min-width: $screen-sm-min) {
- float: right;
- margin-left: $gl-padding;
- margin-top: 2px;
+ width: 300px;
+ text-align: right;
font-size: 0;
}
.ci-status-link {
display: inline-block;
+ position: relative;
+ top: 1px;
}
.btn-clipboard,
@@ -82,40 +148,18 @@
font-weight: 600;
}
-.commit {
- padding: 10px 0;
- position: relative;
-
- @media (min-width: $screen-sm-min) {
- padding-left: 46px;
- }
-
- &:not(:last-child) {
- border-bottom: 1px solid #eee;
- }
+.commit,
+.generic_commit_status {
a,
button {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
vertical-align: baseline;
}
-
- .avatar {
- margin-left: -46px;
- }
-
- .item-title {
- display: inline-block;
-
- @media (min-width: $screen-sm-min) {
- max-width: 70%;
- }
- }
-
.commit-row-description {
font-size: 14px;
- border-left: 1px solid $btn-gray-hover;
+ border-left: 1px solid $white-normal;
padding: 10px 15px;
margin: 10px 0;
background: $gray-light;
@@ -132,20 +176,7 @@
}
a {
- color: $gl-dark-link-color;
- }
- }
-
- .commit-row-info {
- color: $gl-gray;
- line-height: 1.35;
-
- a {
- color: $gl-gray;
- }
-
- .avatar {
- margin-right: 8px;
+ color: $gl-text-color;
}
}
@@ -162,7 +193,7 @@
}
.branch-commit {
- color: $gl-gray;
+ color: $gl-text-color;
.commit-icon {
text-align: center;
@@ -172,7 +203,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
- fill: $table-text-gray;
+ fill: $gl-text-color-secondary;
}
}
@@ -181,62 +212,6 @@
}
.commit-row-message {
- color: $gl-gray;
- }
-}
-
-.divergence-graph {
- padding: 12px 12px 0 0;
- float: right;
-
- .graph-side {
- position: relative;
- width: 80px;
- height: 22px;
- padding: 5px 0 13px;
- float: left;
-
- .bar {
- position: absolute;
- height: 4px;
- background-color: #ccc;
- }
-
- .bar-behind {
- right: 0;
- border-radius: 3px 0 0 3px;
- }
-
- .bar-ahead {
- left: 0;
- border-radius: 0 3px 3px 0;
- }
-
- .count {
- padding-top: 6px;
- padding-bottom: 0;
- font-size: 12px;
- color: #333;
- display: block;
- }
-
- .count-behind {
- padding-right: 4px;
- text-align: right;
- }
-
- .count-ahead {
- padding-left: 4px;
- text-align: left;
- }
- }
-
- .graph-separator {
- position: relative;
- width: 1px;
- height: 18px;
- margin: 5px 0 0;
- float: left;
- background-color: #ccc;
+ color: $gl-text-color;
}
}
diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss
deleted file mode 100644
index 81e5cee240d..00000000000
--- a/app/assets/stylesheets/pages/confirmation.scss
+++ /dev/null
@@ -1,32 +0,0 @@
-.well-confirmation {
- margin-bottom: 20px;
- border-bottom: 1px solid #eee;
-
- > h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- font-weight: 400;
- }
-
- .lead {
- margin-bottom: 20px;
- }
-
- ul,
- ol {
- padding-left: 0;
- }
-
- li {
- list-style-type: none;
- }
-}
-
-.confirmation-content {
- a {
- color: $md-link-color;
- }
-}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 572e1e7d558..cda069e6c0e 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -1,16 +1,63 @@
#cycle-analytics {
+ max-width: 1000px;
margin: 24px auto 0;
- max-width: 800px;
position: relative;
- .panel {
+ .col-headers {
+ ul {
+ margin: 0;
+ padding: 0;
+ @include clearfix;
+ }
+
+ li {
+ display: inline-block;
+ float: left;
+ line-height: 50px;
+ width: 20%;
+ }
+
+
+ .fa {
+ color: $cycle-analytics-light-gray;
+
+ &:hover {
+ color: $gl-text-color;
+ }
+ }
+
+ .stage-header {
+ width: 26%;
+ padding-left: $gl-padding;
+ }
+
+ .median-header {
+ width: 14%;
+ }
+
+ .event-header {
+ width: 45%;
+ padding-left: $gl-padding;
+ }
+
+ .total-time-header {
+ width: 15%;
+ text-align: right;
+ padding-right: $gl-padding;
+ }
+ .stage-name {
+ font-weight: 600;
+ }
+ }
+
+ .panel {
.content-block {
padding: 24px 0;
border-bottom: none;
position: relative;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
padding: 6px 0 24px;
}
}
@@ -18,7 +65,7 @@
.column {
text-align: center;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
padding: 15px 0;
}
@@ -35,23 +82,20 @@
}
&:last-child {
- text-align: right;
-
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
text-align: center;
}
}
}
+ }
- .dropdown {
- top: 13px;
- }
+ .js-ca-dropdown {
+ top: $gl-padding-top;
}
.bordered-box {
border: 1px solid $border-color;
border-radius: $border-radius-default;
-
}
.content-list {
@@ -69,16 +113,16 @@
&.title {
line-height: 19px;
- font-size: 15px;
+ font-size: 14px;
font-weight: 600;
- color: $gl-title-color;
+ color: $gl-text-color;
}
&.text {
color: $layout-link-gray;
&.value-col {
- color: $gl-title-color;
+ color: $gl-text-color;
}
}
}
@@ -103,7 +147,7 @@
position: absolute;
right: $cycle-analytics-box-padding;
cursor: pointer;
- color: #b2b2b2;
+ color: $cycle-analytics-dismiss-icon-color;
}
.svg-container {
@@ -116,7 +160,7 @@
}
.inner-content {
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
padding: 0 28px;
text-align: center;
}
@@ -141,4 +185,302 @@
margin-top: 36px;
}
+ .stage-panel-body {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .stage-nav,
+ .stage-entries {
+ display: flex;
+ vertical-align: top;
+ font-size: $gl-font-size;
+ }
+
+ .stage-nav {
+ width: 40%;
+ margin-bottom: 0;
+
+ ul {
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ }
+
+ li {
+ list-style-type: none;
+ @include clearfix;
+ }
+
+ .stage-nav-item {
+ display: block;
+ line-height: 65px;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ border-right: 1px solid $border-color;
+ background-color: $gray-light;
+
+ &.active {
+ background-color: transparent;
+ border-right-color: transparent;
+ border-top-color: $border-color;
+ border-bottom-color: $border-color;
+ box-shadow: inset 2px 0 0 0 $active-item-blue;
+
+ .stage-name {
+ font-weight: 600;
+ }
+ }
+
+ &:hover:not(.active) {
+ background-color: $gray-lightest;
+ box-shadow: inset 2px 0 0 0 $border-color;
+ cursor: pointer;
+ }
+
+ &:first-child {
+ border-top: none;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .stage-nav-item-cell {
+ float: left;
+
+ &.stage-name {
+ width: 65%;
+ }
+
+ &.stage-median {
+ width: 35%;
+ }
+ }
+
+ .stage-name {
+ padding-left: 16px;
+ }
+
+ .stage-empty,
+ .not-available {
+ color: $gl-text-color-secondary;
+ }
+ }
+ }
+
+ .stage-panel-container {
+ width: 100%;
+ overflow: auto;
+ }
+
+ .stage-panel {
+ min-width: 968px;
+
+ .panel-heading {
+ padding: 0;
+ background-color: transparent;
+ }
+
+ .events-description {
+ line-height: 65px;
+ padding-left: $gl-padding;
+ }
+ }
+
+ .stage-events {
+ width: 60%;
+ overflow: scroll;
+ height: 467px;
+ }
+
+ .stage-event-list {
+ margin: 0;
+ padding: 0;
+ }
+
+ .stage-event-item {
+ list-style-type: none;
+ padding: 0 0 $gl-padding;
+ margin: 0 $gl-padding $gl-padding;
+ border-bottom: 1px solid $gray-darker;
+ @include clearfix;
+
+ &:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+ }
+
+ .item-details,
+ .item-time {
+ float: left;
+ }
+
+ .item-details {
+ width: 75%;
+ }
+
+ .item-title {
+ margin: 0 0 2px;
+
+ &.issue-title,
+ &.commit-title,
+ &.merge-merquest-title {
+ max-width: 100%;
+ display: block;
+ @include text-overflow();
+
+ a {
+ color: $gl-text-color;
+ }
+ }
+ }
+
+ .item-time {
+ width: 25%;
+ text-align: right;
+ }
+
+ .total-time {
+ font-size: $cycle-analytics-big-font;
+ color: $cycle-analytics-dark-text;
+
+ span {
+ color: $gl-text-color;
+ font-size: $gl-font-size;
+ }
+ }
+
+ .issue-date,
+ .build-date {
+ color: $gl-text-color;
+ }
+
+ .issue-link,
+ .commit-author-link,
+ .issue-author-link {
+ color: $gl-text-color;
+ }
+
+ // Custom CSS for components
+ .item-conmmit-component {
+ .commit-icon {
+ position: relative;
+ top: 3px;
+ left: 1px;
+ display: inline-block;
+
+ svg {
+ float: left;
+ }
+ }
+ }
+
+ .merge-request-branch {
+ a {
+ max-width: 180px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ display: inline-block;
+ vertical-align: bottom;
+ }
+ }
+ }
+
+ // Custom Styles for stage items
+ .item-build-component {
+
+ .item-title {
+ .icon-build-status {
+ float: left;
+ margin-right: 5px;
+ position: relative;
+ top: 2px;
+ }
+
+ .item-build-name {
+ color: $gl-text-color;
+ }
+
+ .pipeline-id {
+ color: $gl-text-color;
+ padding: 0 3px 0 0;
+ }
+
+ .branch-name {
+ color: $black;
+ display: inline-block;
+ max-width: 180px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ line-height: 1.3;
+ vertical-align: top;
+ }
+
+ .short-sha {
+ color: $gl-link-color;
+ line-height: 1.3;
+ vertical-align: top;
+ font-weight: normal;
+ }
+
+ .fa {
+ color: $gl-text-color-secondary;
+ font-size: $code_font_size;
+ }
+ }
+ }
+
+ .empty-stage,
+ .no-access-stage {
+ text-align: center;
+ width: 75%;
+ margin: 0 auto;
+ padding-top: 130px;
+ color: $gl-text-color-secondary;
+
+ h4 {
+ color: $gl-text-color;
+ }
+ }
+
+ .empty-stage {
+ .icon-no-data {
+ height: 36px;
+ width: 78px;
+ display: inline-block;
+ margin-bottom: 20px;
+ }
+ }
+
+ .no-access-stage {
+ .icon-lock {
+ height: 36px;
+ width: 78px;
+ display: inline-block;
+ margin-bottom: 20px;
+ }
+ }
+}
+
+.cycle-analytics-overview {
+ padding-top: 100px;
+
+ .overview-details {
+ display: flex;
+ align-items: center;
+ }
+
+ .overview-image {
+ text-align: right;
+ }
+
+ .overview-icon {
+ svg {
+ width: 365px;
+ height: 227px;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
deleted file mode 100644
index 016bab104eb..00000000000
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-.dashboard {
- .side {
- .panel {
- .panel-heading {
- background: $background-color;
- border-top-left-radius: 0;
- }
-
- border-top-left-radius: 0;
- }
- }
-}
-
-.dashboard-search-filter {
- padding: 5px;
-
- .search-text-input {
- float: left;
- @extend .col-md-2;
- }
-
- .btn {
- margin-left: 5px;
- float: left;
- }
-}
-
-.project-access-icon {
- margin-left: 10px;
- float: left;
- margin-right: 15px;
- margin-bottom: 15px;
-
- i {
- color: #888;
- }
-}
-
-.dash-project-access-icon {
- float: left;
- margin-right: 5px;
- width: 16px;
-}
diff --git a/app/assets/stylesheets/pages/deploy_keys.scss b/app/assets/stylesheets/pages/deploy_keys.scss
new file mode 100644
index 00000000000..2fafe052106
--- /dev/null
+++ b/app/assets/stylesheets/pages/deploy_keys.scss
@@ -0,0 +1,13 @@
+.deploy-keys-list {
+ width: 100%;
+ overflow: auto;
+
+ table {
+ border: 1px solid $table-border-color;
+ }
+}
+
+.deploy-keys-title {
+ padding-bottom: 2px;
+ line-height: 2;
+}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 0f0c0abe7ae..46fd19c93f9 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -1,16 +1,15 @@
.detail-page-header {
padding: $gl-padding-top 0;
border-bottom: 1px solid $border-color;
- color: #5c5d5e;
- font-size: 16px;
+ color: $gl-text-color;
line-height: 34px;
.author {
- color: #5c5d5e;
+ color: $gl-text-color;
}
.identifier {
- color: #5c5d5e;
+ color: $gl-text-color;
}
.issue_created_ago,
@@ -23,7 +22,7 @@
.title {
margin: 0 0 16px;
font-size: 2em;
- color: $gl-gray-dark;
+ color: $gl-text-color;
padding: 0 0 0.3em;
border-bottom: 1px solid $white-dark;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index fde138c874d..96ba7c40634 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -11,10 +11,10 @@
.diff-header {
position: relative;
- background: $background-color;
+ background: $gray-light;
border-bottom: 1px solid $border-color;
padding: 10px 16px;
- color: #555;
+ color: $gl-text-color;
z-index: 10;
border-radius: 3px 3px 0 0;
@@ -24,7 +24,7 @@
display: block;
.file-mode {
- color: #777;
+ color: $file-mode-changed;
}
}
@@ -38,7 +38,7 @@
cursor: pointer;
&:hover {
- background-color: $dark-background-color;
+ background-color: $gray-normal;
}
.diff-toggle-caret {
@@ -49,8 +49,8 @@
.diff-content {
overflow: auto;
overflow-y: hidden;
- background: #fff;
- color: #333;
+ background: $white-light;
+ color: $gl-text-color;
border-radius: 0 0 3px 3px;
.unfold {
@@ -59,7 +59,7 @@
.file-mode-changed {
padding: 10px;
- color: #777;
+ color: $file-mode-changed;
}
.suppressed-container {
@@ -92,20 +92,6 @@
&.noteable_line {
position: relative;
-
- &.old {
- &::before {
- content: '-';
- position: absolute;
- }
- }
-
- &.new {
- &::before {
- content: '+';
- position: absolute;
- }
- }
}
span {
@@ -151,8 +137,9 @@
.line_content {
display: block;
margin: 0;
- padding: 0 0.5em;
+ padding: 0 1.5em;
border: none;
+ position: relative;
&.parallel {
display: table-cell;
@@ -161,6 +148,22 @@
word-break: break-all;
}
}
+
+ &.old {
+ &::before {
+ content: '-';
+ position: absolute;
+ left: 0.5em;
+ }
+ }
+
+ &.new {
+ &::before {
+ content: '+';
+ position: absolute;
+ left: 0.5em;
+ }
+ }
}
.text-file.diff-wrap-lines table .line_holder td span {
@@ -169,7 +172,7 @@
}
.image {
- background: #ddd;
+ background: $diff-image-bg;
text-align: center;
padding: 30px;
@@ -179,13 +182,13 @@
.frame {
display: inline-block;
- background-color: #fff;
+ background-color: $white-light;
line-height: 0;
img {
- border: 1px solid #fff;
- background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%),
- linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%);
+ border: 1px solid $white-light;
+ background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%),
+ linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%);
background-size: 10px 10px;
background-position: 0 0, 5px 5px;
max-width: 100%;
@@ -203,7 +206,7 @@
.image-info {
font-size: 12px;
margin: 5px 0 0;
- color: grey;
+ color: $diff-image-info-color;
}
.view.swipe {
@@ -217,7 +220,7 @@
.swipe-wrap {
overflow: hidden;
- border-left: 1px solid #999;
+ border-left: 1px solid $diff-swipe-border;
position: absolute;
display: block;
top: 13px;
@@ -347,7 +350,7 @@
.view-modes {
padding: 10px;
text-align: center;
- background: #eee;
+ background: $gray-darker;
ul,
li {
@@ -358,8 +361,8 @@
}
li {
- color: grey;
- border-left: 1px solid #c1c1c1;
+ color: $diff-view-modes-color;
+ border-left: 1px solid $diff-view-modes-border;
padding: 0 12px 0 16px;
cursor: pointer;
@@ -377,7 +380,7 @@
}
cursor: default;
- color: #333;
+ color: $gl-text-color;
}
&.disabled {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 778126bcfb7..4af267403d8 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -10,14 +10,14 @@
}
.ace_gutter-cell {
- background-color: $background-color;
+ background-color: $gray-light;
}
.cancel-btn {
- color: #b94a48;
+ color: $editor-cancel-color;
&:hover {
- color: #b94a48;
+ color: $editor-cancel-color;
}
}
@@ -34,7 +34,7 @@
}
.editor-ref {
- background: $background-color;
+ background: $gray-light;
padding-right: $gl-padding;
border-right: 1px solid $border-color;
display: block;
@@ -51,8 +51,16 @@
.new-file-name {
display: inline-block;
- width: 450px;
+ max-width: 450px;
float: left;
+
+ @media(max-width: $screen-md-max) {
+ width: 280px;
+ }
+
+ @media(max-width: $screen-sm-max) {
+ width: 180px;
+ }
}
.file-buttons {
@@ -67,7 +75,8 @@
.soft-wrap-toggle,
.license-selector,
.gitignore-selector,
- .gitlab-ci-yml-selector {
+ .gitlab-ci-yml-selector,
+ .dockerfile-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
@@ -97,7 +106,8 @@
.gitignore-selector,
.license-selector,
- .gitlab-ci-yml-selector {
+ .gitlab-ci-yml-selector,
+ .dockerfile-selector {
.dropdown {
line-height: 21px;
}
@@ -114,3 +124,42 @@
}
}
}
+
+@media(max-width: $screen-xs-max){
+ .file-editor {
+ .file-title {
+ .pull-right {
+ height: auto;
+ }
+ }
+
+ .new-file-name {
+ max-width: none;
+ width: 100%;
+ margin-bottom: 3px;
+ }
+
+ .file-buttons {
+ display: block;
+ width: 100%;
+ margin-bottom: 10px;
+
+ .soft-wrap-toggle {
+ width: 100%;
+ margin: 3px 0;
+ }
+
+ .encoding-selector,
+ .license-selector,
+ .gitignore-selector,
+ .gitlab-ci-yml-selector {
+ display: block;
+ margin: 3px 0;
+
+ button {
+ width: 100%;
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index fc49ff780fc..778ef01430e 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,10 +1,56 @@
-.environments-container,
-.deployments-container {
+@media (max-width: $screen-md-max) {
+ .deployments-container {
+ width: 100%;
+ overflow: auto;
+ }
+}
+
+.environments-list-loading {
width: 100%;
- overflow: auto;
+ font-size: 34px;
+}
+
+@media (max-width: $screen-xs-max) {
+ .environments-container {
+ width: 100%;
+ overflow: auto;
+ }
}
.environments {
+ table-layout: fixed;
+
+ .environments-commit,
+ .environments-actions,
+ .environments-deploy,
+ .environments-build,
+ .environments-date {
+ position: static;
+ float: none;
+ display: table-cell;
+ }
+
+ .environments-name,
+ .environments-commit,
+ .environments-actions {
+ width: 20%;
+ }
+
+ .environments-date {
+ width: 10%;
+ }
+
+ .environments-deploy,
+ .environments-build {
+ width: 15%;
+ }
+
+ .environment-name,
+ .environments-build-cell,
+ .deployment-column {
+ word-break: break-all;
+ }
+
.deployment-column {
.avatar {
float: none;
@@ -15,6 +61,10 @@
margin: 0;
}
+ .avatar-image-container {
+ text-decoration: none;
+ }
+
.icon-play {
height: 13px;
width: 12px;
@@ -22,24 +72,25 @@
.external-url,
.dropdown-new {
- color: $table-text-gray;
+ color: $gl-text-color-secondary;
}
.dropdown-menu {
.fa {
margin-right: 6px;
- color: $table-text-gray;
+ color: $gl-text-color-secondary;
}
}
.build-link,
.branch-name {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
- .stop-env-link {
- color: $table-text-gray;
+ .stop-env-link,
+ .external-url {
+ color: $gl-text-color-secondary;
.stop-env-icon {
font-size: 14px;
@@ -50,7 +101,7 @@
.build-column {
.build-link {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
.avatar {
@@ -58,10 +109,22 @@
}
}
}
+
+ .children-row .environment-name {
+ margin-left: 17px;
+ margin-right: -17px;
+ }
+
+ .folder-icon {
+ padding: 0 5px 0 0;
+ }
+
+ .folder-name {
+ cursor: pointer;
+ }
}
.table.ci-table.environments {
-
.icon-container {
width: 20px;
text-align: center;
@@ -72,4 +135,4 @@
margin-right: 0;
}
}
-}
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/errors.scss b/app/assets/stylesheets/pages/errors.scss
deleted file mode 100644
index 11309817d31..00000000000
--- a/app/assets/stylesheets/pages/errors.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-.error-page {
- max-width: 400px;
- margin: 0 auto;
-
- h1,
- h2,
- h3 {
- text-align: center;
- }
-
- h1 {
- font-size: 56px;
- line-height: 100px;
- font-weight: 300;
- }
-}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 3004959ff7b..b989d72ce1c 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -5,7 +5,7 @@
.event-item {
font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
- border-bottom: 1px solid $table-border-color;
+ border-bottom: 1px solid $white-normal;
color: $list-text-color;
&.event-inline {
@@ -21,7 +21,7 @@
}
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
.avatar {
@@ -62,7 +62,7 @@
border: none;
background: $gray-light;
border-radius: 0;
- color: #777;
+ color: $events-pre-color;
margin: 0 20px;
overflow: hidden;
}
@@ -80,7 +80,7 @@
}
.event-note-icon {
- color: #777;
+ color: $events-pre-color;
float: left;
font-size: $gl-font-size;
line-height: 16px;
@@ -91,7 +91,7 @@
.event_icon {
position: relative;
float: right;
- border: 1px solid #eee;
+ border: 1px solid $gray-darker;
padding: 5px;
border-radius: 5px;
background: $gray-light;
@@ -170,7 +170,7 @@
.event-body {
margin: 0;
- border-left: 2px solid #ddd;
+ border-left: 2px solid $events-body-border;
padding-left: 10px;
}
@@ -186,4 +186,3 @@
display: none;
}
}
-
diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss
deleted file mode 100644
index 9b92128624c..00000000000
--- a/app/assets/stylesheets/pages/explore.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.explore-title {
- text-align: center;
-
- h3 {
- font-weight: normal;
- font-size: 30px;
- }
-}
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index f7f9a9bb770..84da9180f93 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -2,15 +2,15 @@
border: 1px solid $border-color;
.controls {
- color: #888;
+ color: $project-network-controls-color;
font-size: 14px;
padding: 5px;
border-bottom: 1px solid $border-color;
- background: #eee;
+ background: $gray-darker;
}
.network-graph {
- background: #fff;
+ background: $white-light;
height: 500px;
overflow-y: scroll;
overflow-x: hidden;
@@ -20,15 +20,14 @@
.graphs {
.graph-author-email {
float: right;
- color: #777;
+ color: $graph-author-email-color;
}
.graph-additions {
- color: #4a2;
+ color: $gl-text-green;
}
.graph-deletions {
- color: #d12f19;
+ color: $gl-text-red;
}
}
-
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 4375e29c8db..d377526e655 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -10,11 +10,10 @@
}
.group-row {
-
.stats {
float: right;
line-height: $list-text-height;
- color: $gl-gray;
+ color: $gl-text-color;
span {
margin-right: 15px;
@@ -23,36 +22,12 @@
}
.ldap-group-links {
-
.form-actions {
margin-bottom: $gl-padding;
}
}
-.groups-cover-block {
-
- .container-fluid {
- position: relative;
- }
-
- .group-right-buttons {
- position: absolute;
- right: 16px;
-
- .btn {
- @include btn-gray;
- padding: 3px 10px;
- background-color: $background-color;
- }
- }
-
- .group-avatar {
- border: 0;
- }
-}
-
.groups-header {
-
@media (min-width: $screen-sm-min) {
.nav-links {
width: 35%;
@@ -68,14 +43,14 @@
padding: 50px 100px;
overflow: hidden;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
padding: 50px 0;
}
svg {
float: right;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
float: none;
display: block;
width: 250px;
@@ -90,7 +65,7 @@
width: 460px;
margin-top: 120px;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
float: none;
margin-top: 60px;
width: auto;
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index a48b4c65db8..dae8ccdef6c 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -9,7 +9,7 @@
li {
line-height: 24px;
- color: #888;
+ color: $document-index-color;
a {
margin-right: 3px;
@@ -20,7 +20,7 @@
.shortcut-mappings {
font-size: 12px;
- color: #555;
+ color: $help-shortcut-mapping-color;
tbody:first-child tr:first-child {
padding-top: 0;
@@ -29,7 +29,7 @@
th {
padding-top: 15px;
line-height: 1.5;
- color: #333;
+ color: $help-shortcut-header-color;
text-align: left;
}
@@ -42,7 +42,7 @@
.shortcut {
padding-right: 10px;
- color: #999;
+ color: $help-shortcut-color;
text-align: right;
white-space: nowrap;
}
@@ -60,7 +60,7 @@
// Border around images in the help pages.
img:not(.emoji) {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
padding: 5px;
margin: 5px;
max-height: calc(100vh - 100px);
diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/pages/icons.scss
deleted file mode 100644
index 407c8db211d..00000000000
--- a/app/assets/stylesheets/pages/icons.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-// CI icon colors
-
-.ci-status-icon {
- &-created {
- fill: $gray-darkest;
- }
-
- &-skipped,
- &-canceled {
- fill: $gl-text-color;
- }
-}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 230b927a17d..93cc5a8cf0a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,3 +1,52 @@
+// Limit MR description for side-by-side diff view
+.fixed-width-container {
+ max-width: $limited-layout-width - ($gl-padding * 2);
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.limit-container-width {
+ .detail-page-header {
+ @extend .fixed-width-container;
+ }
+
+ .issuable-details {
+ .detail-page-description,
+ .mr-source-target,
+ .mr-state-widget,
+ .merge-manually {
+ @extend .fixed-width-container;
+ }
+
+ .merge-request-tabs-holder {
+ &.affix {
+ border-bottom: 1px solid $border-color;
+
+ .nav-links {
+ border: 0;
+ }
+ }
+
+ .container-fluid {
+ @extend .fixed-width-container;
+ }
+ }
+ }
+
+ .merge-request-details {
+ .emoji-list-container {
+ @extend .fixed-width-container;
+ }
+ }
+
+ .diffs {
+ .mr-version-controls,
+ .files-changed {
+ @extend .fixed-width-container;
+ }
+ }
+}
+
.issuable-details {
section {
.issuable-discussion {
@@ -5,11 +54,16 @@
}
}
+ .title {
+ padding: 0;
+ margin: 0;
+ border-bottom: none;
+ }
+
// Border around images in issue and MR descriptions.
.description img:not(.emoji) {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
padding: 5px;
- margin: 5px;
max-height: calc(100vh - 100px);
}
}
@@ -30,6 +84,7 @@
.color-label {
padding: 6px 10px;
+ border-radius: $label-border-radius;
}
}
@@ -50,7 +105,7 @@
.block {
@include clearfix;
padding: $gl-padding 0;
- border-bottom: 1px solid $border-gray-light;
+ border-bottom: 1px solid $border-gray-normal;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter_inner_width;
@@ -97,10 +152,10 @@
}
.edit-link {
- color: $gl-gray;
+ color: $gl-text-color;
- &:hover {
- color: $md-link-color;
+ &:not([href]):hover {
+ color: rgba($avatar-border, .2);
}
}
}
@@ -132,8 +187,8 @@
display: none;
}
- .btn-clipboard {
- color: $gl-gray;
+ .btn-clipboard:hover {
+ color: $gl-text-color;
}
}
@@ -168,7 +223,7 @@
}
.no-value {
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
}
.sidebar-collapsed-icon {
@@ -177,7 +232,7 @@
.gutter-toggle {
margin-top: 7px;
- border-left: 1px solid $border-gray-light;
+ border-left: 1px solid $border-gray-normal;
}
.assignee .avatar {
@@ -215,7 +270,7 @@
}
.participants {
- border-bottom: 1px solid $border-gray-light;
+ border-bottom: 1px solid $border-gray-normal;
}
.hide-collapsed {
@@ -233,7 +288,11 @@
width: 100%;
text-align: center;
padding-bottom: 10px;
- color: #999;
+ color: $issuable-sidebar-color;
+
+ &:hover {
+ color: $gl-text-color;
+ }
span {
display: block;
@@ -244,15 +303,17 @@
display: none;
}
+ .avatar:hover {
+ border-color: $issuable-sidebar-color;
+ }
+
.btn-clipboard {
border: none;
+ color: $issuable-sidebar-color;
&:hover {
background: transparent;
- }
-
- i {
- color: #999;
+ color: $gl-text-color;
}
}
}
@@ -267,24 +328,14 @@
}
}
- .issuable-header-btn {
- background: $gray-normal;
- border: 1px solid $border-gray-normal;
-
- &:hover {
- background: $gray-dark;
- border: 1px solid $border-gray-dark;
- }
-
- &.btn-primary {
- @extend .btn-primary;
- }
- }
-
a {
&:hover {
color: $md-link-color;
text-decoration: none;
+
+ .avatar {
+ border-color: rgba($avatar-border, .2);
+ }
}
}
@@ -326,6 +377,10 @@
display: inline-block;
padding: 5px;
+ &:nth-of-type(7n) {
+ padding-right: 0;
+ }
+
.author_link {
display: block;
}
@@ -340,7 +395,7 @@
margin-left: 5px;
a {
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
}
}
@@ -395,6 +450,7 @@
.issuable-meta {
display: inline-block;
line-height: 18px;
+ font-size: 14px;
}
.js-issuable-selector-wrap {
@@ -421,3 +477,102 @@
}
}
}
+
+.time_tracker {
+ padding-bottom: 0;
+ border-bottom: 0;
+
+
+ .sidebar-collapsed-icon {
+
+ > .stopwatch-svg {
+ display: inline-block;
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $sidebar-collapsed-icon-color;
+ }
+
+ &:hover svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ .help-button,
+ .close-help-button {
+ cursor: pointer;
+ }
+
+ .compare-meter {
+ &.within_estimate {
+ .meter-fill {
+ background: $gl-primary;
+ }
+ }
+
+ &.over_estimate {
+ .meter-fill {
+ background: $red-light;
+ }
+
+ .time-remaining,
+ .compare-value.spent {
+ color: $red-light;
+ }
+ }
+ }
+
+ .meter-container {
+ background: $border-gray-light;
+ border-radius: 3px;
+
+ .meter-fill {
+ max-width: 100%;
+ height: 5px;
+ border-radius: 3px;
+ background: $gl-primary;
+ }
+ }
+
+ .compare-display-container {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 5px;
+
+ .compare-display {
+ font-size: 13px;
+ color: $compare-display-color;
+
+ .compare-value {
+ color: $gl-text-color;
+ }
+ }
+ }
+
+ .time-tracking-help-state {
+ background: $white-light;
+ margin: 16px -20px 0;
+ padding: 16px 20px;
+ border-top: 1px solid $border-gray-light;
+ border-bottom: 1px solid $border-gray-light;
+
+ a:hover {
+ color: $btn-white-active;
+ }
+ }
+
+ .help-state-toggle-enter-active {
+ transition: all .8s ease;
+ }
+
+ .help-state-toggle-leave-active {
+ transition: all .5s ease;
+ }
+
+ .help-state-toggle-enter,
+ .help-state-toggle-leave-active {
+ opacity: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 3e7fc3fa52c..8734a3b1598 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -55,26 +55,45 @@ ul.related-merge-requests > li {
}
.merge-request-status {
- color: $gl-gray;
- font-size: 15px;
- font-weight: bold;
+ font-size: 13px;
+ padding: 0 5px;
+ color: $white-light;
+ height: 20px;
+ border-radius: 3px;
+ line-height: 18px;
+ border: 1px solid;
+
+ &.merged {
+ border-color: darken($blue-normal, 10%);
+ background: $blue-normal;
+ }
+
+ &.closed {
+ border-color: darken($red-normal, 10%);
+ background: $red-normal;
+ }
+
+ &.open {
+ border: 1px solid darken($green-normal, 10%);
+ background: $green-normal;
+ }
}
.merge-request,
.issue {
&.today {
- background: #f3fff2;
- border-color: #e1e8d5;
+ background: $issues-today-bg;
+ border-color: $issues-today-border;
}
&.closed {
background: $gray-light;
- border-color: #e5e5e5;
+ border-color: $border-color;
}
&.merged {
background: $gray-light;
- border-color: #e5e5e5;
+ border-color: $border-color;
}
}
@@ -125,7 +144,7 @@ ul.related-merge-requests > li {
}
.btn {
- background-color: $background-color;
- border: 1px solid $border-gray-light;
+ background-color: $gray-light;
+ border: 1px solid $border-gray-normal;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 397f89f501a..21d9b4c54ea 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -90,7 +90,7 @@
@media (min-width: $screen-sm-min) {
display: inline-block;
- width: 40%;
+ width: 30%;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
@@ -98,13 +98,14 @@
}
.label {
- padding: 9px;
+ padding: 8px 9px 9px;
font-size: 14px;
}
}
.color-label {
- padding: 3px 4px;
+ padding: 3px 7px;
+ border-radius: $label-border-radius;
}
.dropdown-labels-error {
@@ -116,7 +117,7 @@
.manage-labels-list {
.btn-action {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
.fa {
font-size: 18px;
@@ -198,8 +199,14 @@
}
.label-remove {
- border-left: 1px solid rgba(0, 0, 0, .1);
+ border-left: 1px solid $label-remove-border;
z-index: 3;
+ border-radius: $label-border-radius;
+ padding: 6px 10px 6px 9px;
+
+ &:hover {
+ box-shadow: inset 0 0 0 80px $label-remove-border;
+ }
}
.btn {
@@ -222,6 +229,14 @@
width: 100%;
}
+.label-subscription {
+ vertical-align: middle;
+
+ .dropdown-group-label a {
+ cursor: pointer;
+ }
+}
+
.label-subscribe-button {
.label-subscribe-button-icon {
&[disabled] {
diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss
index 8290519dc25..68b6c5ecbd4 100644
--- a/app/assets/stylesheets/pages/lint.scss
+++ b/app/assets/stylesheets/pages/lint.scss
@@ -1,11 +1,21 @@
.ci-body {
.incorrect-syntax {
- font-size: 19px;
- color: red;
+ font-size: 18px;
+ color: $lint-incorrect-color;
}
.correct-syntax {
- font-size: 19px;
- color: #47a447;
+ font-size: 18px;
+ color: $lint-correct-color;
+ }
+}
+
+.ci-linter {
+ .ci-editor {
+ height: 400px;
+ }
+
+ .ci-template pre {
+ white-space: pre-wrap;
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 10f67b47998..71ed5b1361a 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -17,14 +17,19 @@
line-height: 1.5;
p {
- font-size: 18px;
- color: #888;
+ font-size: 16px;
+ color: $login-brand-holder-color;
}
h1:first-child {
font-weight: normal;
- margin-bottom: 30px;
+ margin-bottom: 0.68em;
margin-top: 0;
+ font-size: 34px;
+ }
+
+ h3 {
+ font-size: 22px;
}
img {
@@ -105,19 +110,19 @@
li {
flex: 1;
text-align: center;
+ border-left: 1px solid $border-color;
&:first-of-type {
+ border-left: none;
border-top-left-radius: $border-radius-default;
}
&:last-of-type {
- border-left: 1px solid $border-color;
border-top-right-radius: $border-radius-default;
}
&:not(.active) {
background-color: $gray-light;
- border-left: 1px solid $border-color;
}
a {
@@ -174,7 +179,7 @@
.form-control {
&:active,
&:focus {
- background-color: #fff;
+ background-color: $white-light;
}
}
@@ -195,7 +200,7 @@
h2 {
margin-top: 0;
font-size: 14px;
- color: #a00;
+ color: $login-devise-error-color;
}
}
}
@@ -254,27 +259,3 @@
}
}
}
-
-// For sign in pane only, to improve tab order, the following removes the submit button from
-// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928
-
-.login-box {
- .new_user {
- position: relative;
- padding-bottom: 35px;
-
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- .forgot-password {
- float: none !important;
- margin-top: 5px;
- }
- }
- }
-
- .move-submit-down {
- position: absolute;
- width: 100%;
- bottom: 0;
- }
-}
-
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 756efa9c7fa..be7193bae04 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -25,7 +25,7 @@
}
.form-horizontal {
- margin-top: 5px;
+ margin-top: 20px;
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
@@ -54,6 +54,10 @@
@media (min-width: $screen-sm-min) {
width: 50%;
}
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
}
.member-access-text {
@@ -74,19 +78,48 @@
float: right;
}
+ .dropdown {
+ width: 100%;
+ margin-top: 5px;
+
+ .dropdown-menu-toggle {
+ vertical-align: middle;
+ width: 100%;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ margin-top: 0;
+ width: 155px;
+ }
+ }
+
.form-control {
width: 100%;
padding-right: 35px;
@media (min-width: $screen-sm-min) {
+ width: 250px;
+ }
+
+ @media (min-width: $screen-md-min) {
width: 350px;
}
+
+ &.input-short {
+ @media (min-width: $screen-md-min) {
+ width: 170px;
+ }
+
+ @media (min-width: $screen-lg-min) {
+ width: 210px;
+ }
+ }
}
}
.member-search-btn {
position: absolute;
- right: 0;
+ right: 4px;
top: 0;
height: 35px;
padding-left: 10px;
@@ -95,4 +128,8 @@
background: transparent;
border: 0;
outline: 0;
+
+ @media (min-width: $screen-sm-min) {
+ right: 160px;
+ }
}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 19ab198c2e7..5a9f199fb34 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -1,3 +1,5 @@
+// Disabled to use the color map for creating color schemes
+// scss-lint:disable ColorVariable
$colors: (
white_header_head_neutral : #e1fad7,
white_line_head_neutral : #effdec,
@@ -98,6 +100,7 @@ $colors: (
solarized_dark_header_not_chosen : rgba(#839496, .25),
solarized_dark_line_not_chosen : rgba(#839496, .15)
);
+// scss-lint:enable ColorVariable
@mixin color-scheme($color) {
@@ -228,14 +231,15 @@ $colors: (
position: absolute;
right: 10px;
padding: 0;
- color: #fff;
+ outline: none;
+ color: $white-light;
width: 75px; // static width to make 2 buttons have same width
height: 19px;
}
}
.btn-success .fa-spinner {
- color: #fff;
+ color: $white-light;
}
.editor-wrap {
@@ -270,7 +274,7 @@ $colors: (
}
.discard-changes-alert {
- background-color: $background-color;
+ background-color: $gray-light;
text-align: right;
padding: $gl-padding-top $gl-padding;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index f8e31a624ec..45ff9f7ff5f 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,8 +3,8 @@
*
*/
.mr-state-widget {
- background: $background-color;
- color: $gl-gray;
+ background: $gray-light;
+ color: $gl-text-color;
border: 1px solid $border-color;
border-radius: 2px;
@@ -21,10 +21,18 @@
display: inline-block;
float: left;
+ .btn-success.dropdown-toggle .fa {
+ color: inherit;
+ }
+
+ .btn-success.dropdown-toggle:disabled {
+ background-color: $gl-success;
+ }
+
.accept_merge_request {
&.ci-pending,
&.ci-running {
- @include btn-orange;
+ @include btn-blue;
}
&.ci-skipped,
@@ -47,9 +55,10 @@
&.right {
float: right;
+ padding-right: 0;
a {
- color: $gl-gray;
+ color: $gl-text-color;
}
}
@@ -60,7 +69,8 @@
}
.ci_widget {
- border-bottom: 1px solid $widget-inner-border;
+ border-bottom: 1px solid $well-inner-border;
+ color: $gl-text-color;
svg {
margin-right: 4px;
@@ -69,48 +79,12 @@
overflow: visible;
}
- &.ci-success {
- color: $gl-success;
-
- a.environment,
- a.pipeline {
- color: inherit;
- }
- }
-
&.ci-success_with_warnings {
- color: $gl-success;
i {
color: $gl-warning;
}
}
-
- &.ci-skipped {
- background-color: #eee;
- color: #888;
- }
-
- &.ci-pending {
- color: $gl-warning;
- }
-
- &.ci-running {
- color: $blue-normal;
- }
-
- &.ci-failed,
- &.ci-error {
- color: $gl-danger;
- }
-
- &.ci-canceled {
- color: $gl-gray;
- }
-
- a.monospace {
- color: inherit;
- }
}
.mr-widget-body,
@@ -120,7 +94,7 @@
}
.normal {
- color: #5c5d5e;
+ color: $gl-text-color;
}
.js-deployment-link {
@@ -130,9 +104,9 @@
.mr-widget-body {
h4 {
font-weight: 600;
- font-size: 17px;
+ font-size: 16px;
margin: 5px 0;
- color: $gl-gray-dark;
+ color: $gl-text-color;
&.has-conflicts .fa-exclamation-triangle {
color: $gl-warning;
@@ -150,7 +124,7 @@
@media (max-width: $screen-xs-max) {
h4 {
- font-size: 15px;
+ font-size: 14px;
}
p {
@@ -163,6 +137,11 @@
margin-bottom: 4px;
}
+ .btn-grouped {
+ float: none;
+ margin-right: 0;
+ }
+
.accept-action {
width: 100%;
text-align: center;
@@ -177,7 +156,7 @@
}
.mr-widget-footer {
- border-top: 1px solid #eee;
+ border-top: 1px solid $gray-darker;
}
.ci-coverage {
@@ -211,7 +190,7 @@
}
.label-branch {
- color: $gl-gray-dark;
+ color: $gl-text-color;
font-family: $monospace_font;
font-weight: bold;
overflow: hidden;
@@ -331,10 +310,6 @@
left: 0;
top: 2px;
}
-
- .commit-row-info {
- line-height: 20px;
- }
}
.btn-clipboard {
@@ -388,7 +363,7 @@
th {
background-color: $white-light;
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
}
}
}
@@ -404,7 +379,7 @@
}
.mr-version-controls {
- background: $background-color;
+ background: $gray-light;
border-bottom: 1px solid $border-color;
color: $gl-text-color;
@@ -445,11 +420,19 @@
.merge-request-tabs-holder {
background-color: $white-light;
+ .container-limited {
+ max-width: $limited-layout-width;
+ }
+
&.affix {
top: 100px;
left: 0;
z-index: 10;
transition: right .15s;
+
+ @media (max-width: $screen-xs-max) {
+ right: 0;
+ }
}
&:not(.affix) .container-fluid {
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 13402acd8e1..686b64cdd24 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -11,6 +11,7 @@
}
.progress {
+ width: 100%;
height: 6px;
}
}
@@ -24,13 +25,6 @@
}
.issuable-row {
- .color-label {
- border-radius: 2px;
- padding: 3px !important;
- margin-right: 7px;
- }
-
- // Issue title
span a {
color: $gl-text-color;
word-wrap: break-word;
@@ -39,15 +33,66 @@
}
.milestone-summary {
- margin-bottom: 25px;
-
.milestone-stat {
+ white-space: nowrap;
margin-right: 10px;
+
+ &.with-drilldown {
+ margin-right: 2px;
+ }
}
.remaining-days {
color: $orange-light;
}
+
+ .milestone-stats-and-buttons {
+ display: flex;
+ justify-content: flex-start;
+ flex-wrap: wrap;
+
+ @media (min-width: $screen-xs-min) {
+ justify-content: space-between;
+ flex-wrap: nowrap;
+ }
+ }
+
+ .milestone-progress-buttons {
+ order: 1;
+ margin-top: 10px;
+
+ @media (min-width: $screen-xs-min) {
+ order: 2;
+ margin-top: 0;
+ flex-shrink: 0;
+ }
+
+ .btn {
+ float: left;
+ margin-right: $btn-side-margin;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+
+ .milestone-stats {
+ order: 2;
+ width: 100%;
+ padding: 7px 0;
+ flex-shrink: 1;
+
+ @media (min-width: $screen-xs-min) {
+ // when displayed on one line stats go first, buttons second
+ order: 1;
+ }
+ }
+
+ .progress {
+ width: 100%;
+ margin: 15px 0;
+ }
}
.issues-sortable-list,
@@ -57,13 +102,17 @@
margin-top: 7px;
.issuable-number {
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
margin-right: 5px;
}
.avatar {
float: none;
}
+
+ > a:not(:last-of-type) {
+ margin-right: 5px;
+ }
}
}
@@ -72,7 +121,7 @@
padding: 20px 0;
}
-@media (max-width: $screen-sm-min) {
+@media (max-width: $screen-xs-max) {
.milestone-actions {
@include clearfix();
padding-top: $gl-vert-padding;
@@ -82,3 +131,50 @@
}
}
}
+
+.milestone-page-header {
+ display: flex;
+ flex-flow: row;
+ align-items: center;
+ flex-wrap: wrap;
+
+ .status-box {
+ margin-top: 0;
+ }
+
+ .milestone-buttons {
+ margin-left: auto;
+ }
+
+ .status-box {
+ order: 1;
+ }
+
+ .milestone-buttons {
+ order: 2;
+ }
+
+ .header-text-content {
+ order: 3;
+ width: 100%;
+ }
+
+ .milestone-buttons .verbose {
+ display: none;
+ }
+
+ @media (min-width: $screen-xs-min) {
+ .milestone-buttons .verbose {
+ display: inline;
+ }
+
+ .header-text-content {
+ order: 2;
+ width: auto;
+ }
+
+ .milestone-buttons {
+ order: 3;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 16ddef481bd..f984b469609 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -27,6 +27,7 @@
.new-note,
.note-edit-form {
.note-form-actions {
+ position: relative;
margin-top: $gl-padding;
}
@@ -44,7 +45,7 @@
.note-textarea {
display: block;
padding: 10px 0;
- color: $gl-gray;
+ color: $gl-text-color;
font-family: $regular_font;
border: 0;
@@ -62,7 +63,7 @@
.common-note-form {
.md-area {
padding: $gl-padding-top $gl-padding;
- border: 1px solid $note-form-border-color;
+ border: 1px solid $border-color;
border-radius: $border-radius-base;
transition: border-color ease-in-out 0.15s,
box-shadow ease-in-out 0.15s;
@@ -109,9 +110,9 @@
margin: auto;
margin-top: 0;
text-align: center;
- font-size: 13px;
+ font-size: 12px;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
// On smaller devices the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
@@ -129,10 +130,10 @@
.note-edit-form {
display: none;
- font-size: 15px;
+ font-size: 14px;
.md-area {
- background-color: #fff;
+ background-color: $white-light;
}
}
@@ -204,7 +205,7 @@
.comment-toolbar {
padding-top: $gl-padding-top;
- color: $note-toolbar-color;
+ color: $gl-text-color-secondary;
border-top: 1px solid $border-color;
}
@@ -265,3 +266,18 @@
}
}
}
+
+.note-edit-warning.settings-message {
+ display: none;
+ padding: 5px 10px;
+ position: absolute;
+ left: 127px;
+ top: 2px;
+
+ @media (max-width: $screen-xs-max) {
+ position: relative;
+ top: 0;
+ left: 0;
+ margin-bottom: 10px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 526e9ae5cdd..da0caa30c26 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -3,9 +3,9 @@
*/
@-webkit-keyframes targe3-note {
- from { background: #fffff0; }
- 50% { background: #ffffd3; }
- to { background: #fffff0; }
+ from { background: $note-targe3-outside; }
+ 50% { background: $note-targe3-inside; }
+ to { background: $note-targe3-outside; }
}
ul.notes {
@@ -35,11 +35,114 @@ ul.notes {
.system-note {
font-size: 14px;
- padding-top: 10px;
- padding-bottom: 10px;
- background: #fdfdfd;
+ padding: 0;
+ clear: both;
+
+ &.timeline-entry::after {
+ clear: none;
+ }
+
+ .system-note-message {
+ display: inline;
+
+ &::first-letter {
+ text-transform: lowercase;
+ }
+
+ a {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+
+ p {
+ display: inline;
+ margin: 0;
+
+ &::first-letter {
+ text-transform: lowercase;
+ }
+ }
+ }
+
+ .timeline-content {
+ padding: 14px 10px;
+ }
+
+ .note-body {
+ overflow: hidden;
+
+ .system-note-commit-list-toggler {
+ display: none;
+ padding: 10px 0 0;
+ cursor: pointer;
+ position: relative;
+ z-index: 2;
+
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: underline;
+ }
+ }
+
+ .note-text {
+ & p:first-child {
+ display: none;
+ }
+
+ &.system-note-commit-list {
+ max-height: 70px;
+ overflow: hidden;
+ display: block;
+
+ ul {
+ margin: 3px 0 3px 16px !important;
+
+ .gfm-commit {
+ font-family: $monospace_font;
+ font-size: 12px;
+ }
+ }
+
+ p:first-child {
+ display: none;
+ }
+
+ p:last-child {
+ a {
+ color: $gl-text-color;
+
+ &:hover {
+ color: $gl-link-color;
+ }
+ }
+ }
+
+ &::after {
+ content: '';
+ width: 100%;
+ height: 67px;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);
+ }
+
+ &.hide-shade {
+ max-height: 100%;
+ overflow: auto;
+
+ &::after {
+ display: none;
+ background: transparent;
+ }
+ }
+ }
+ }
+ }
.timeline-icon {
+ display: none;
+
.avatar {
visibility: hidden;
@@ -63,7 +166,17 @@ ul.notes {
.note {
display: block;
position: relative;
- border-bottom: 1px solid $table-border-gray;
+ border-bottom: 1px solid $white-normal;
+
+ &.note-discussion {
+ &.timeline-entry {
+ padding: 14px 10px;
+ }
+
+ .system-note {
+ padding: 0;
+ }
+ }
&.is-editting {
.note-header,
@@ -82,16 +195,14 @@ ul.notes {
}
.note-body {
- overflow: auto;
+ overflow-x: auto;
+ overflow-y: hidden;
.note-text {
- overflow: auto;
word-wrap: break-word;
@include md-typography;
-
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
-
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
@@ -141,6 +252,22 @@ ul.notes {
}
}
+.page-sidebar-pinned.right-sidebar-expanded {
+ @media (max-width: $screen-md-max) {
+ .note-header {
+ .note-headline-light {
+ display: block;
+ }
+
+ .note-actions {
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+ }
+ }
+}
+
// Diff code in discussion view
.discussion-body .diff-file {
.file-title {
@@ -164,25 +291,25 @@ ul.notes {
font-family: $regular_font;
td {
- border: 1px solid $table-border-gray;
+ border: 1px solid $white-normal;
border-left: none;
&.notes_line {
vertical-align: middle;
text-align: center;
padding: 10px 0;
- background: $background-color;
+ background: $gray-light;
color: $text-color;
}
&.notes_line2 {
text-align: center;
padding: 10px 0;
- border-left: 1px solid #ddd !important;
+ border-left: 1px solid $note-line2-border !important;
}
&.notes_content {
- background-color: $background-color;
+ background-color: $gray-light;
border-width: 1px 0;
padding: 0;
vertical-align: top;
@@ -218,7 +345,19 @@ ul.notes {
}
.author_link {
- color: $gl-gray;
+ color: $gl-text-color;
+ }
+}
+
+.discussion-header {
+ font-size: 14px;
+}
+
+.note-headline-light {
+ display: inline;
+
+ @media (max-width: $screen-xs-min) {
+ display: block;
}
}
@@ -241,7 +380,7 @@ ul.notes {
.note-actions {
float: right;
margin-left: 10px;
- color: $notes-action-color;
+ color: $gray-darkest;
}
.note-actions {
@@ -252,10 +391,6 @@ ul.notes {
.note-action-button {
margin-left: 10px;
}
-
- @media (min-width: $screen-sm-min) {
- position: relative;
- }
}
.discussion-actions {
@@ -280,9 +415,8 @@ ul.notes {
}
.fa {
- color: $notes-action-color;
+ color: $gray-darkest;
position: relative;
- top: 1px;
font-size: 17px;
}
@@ -318,15 +452,10 @@ ul.notes {
color: $notes-role-color;
font-size: 12px;
line-height: 20px;
- border: 1px solid $notes-role-border-color;
+ border: 1px solid $border-color;
border-radius: $border-radius-base;
}
-.diff-file .note .note-actions {
- right: 0;
- top: 0;
-}
-
/**
* Line note button on the side of diffs
@@ -340,7 +469,7 @@ ul.notes {
.add-diff-note {
margin-top: -4px;
border-radius: 40px;
- background: #fff;
+ background: $white-light;
padding: 4px;
font-size: 16px;
color: $gl-link-color;
@@ -353,7 +482,7 @@ ul.notes {
&:hover {
background: $gl-info;
- color: #fff;
+ color: $white-light;
@include show-add-diff-note;
}
}
@@ -386,7 +515,6 @@ ul.notes {
.line-resolve-all-container {
.btn-group {
- margin-top: -1px;
margin-left: -4px;
}
@@ -397,27 +525,24 @@ ul.notes {
}
.line-resolve-all {
+ vertical-align: middle;
display: inline-block;
- padding: 5px 10px;
- background-color: $background-color;
+ padding: 6px 10px;
+ background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
&.has-next-btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
+ border-right: 0;
}
.line-resolve-btn {
- vertical-align: middle;
margin-right: 5px;
}
}
-.line-resolve-text {
- vertical-align: middle;
-}
-
.line-resolve-btn {
display: inline-block;
position: relative;
@@ -436,18 +561,14 @@ ul.notes {
&.is-active {
color: $gl-text-green;
- svg path {
+ svg {
fill: $gl-text-green;
}
}
svg {
position: relative;
- color: $notes-action-color;
-
- path {
- fill: $notes-action-color;
- }
+ fill: $gray-darkest;
}
}
@@ -460,3 +581,17 @@ ul.notes {
}
}
}
+
+// Merge request notes in diffs
+.diff-file {
+ // Diff is side by side
+ .notes_content.parallel .note-header .note-headline-light {
+ display: block;
+ position: relative;
+ }
+ // Diff is inline
+ .notes_content .note-header .note-headline-light {
+ display: inline-block;
+ position: relative;
+ }
+}
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index 94fbbef3c77..bdf07a99daf 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -1,24 +1,16 @@
.notification-list-item {
line-height: 34px;
+
+ .dropdown-menu {
+ @extend .dropdown-menu-align-right;
+ }
}
.notification {
position: relative;
top: 1px;
- > .fa {
+ .fa {
font-size: 18px;
}
}
-
-.ns-part {
- color: $gl-text-green;
-}
-
-.ns-watch {
- color: $gl-success;
-}
-
-.ns-mute {
- color: $gl-danger;
-}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index bf3cb6e7ad9..8dff22e32bd 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -1,4 +1,9 @@
.pipelines {
+ .realtime-loading {
+ font-size: 40px;
+ text-align: center;
+ }
+
.stage {
max-width: 90px;
width: 90px;
@@ -22,30 +27,44 @@
.table.ci-table {
min-width: 1200px;
+ table-layout: fixed;
+
+ .label {
+ margin-bottom: 3px;
+ }
.pipeline-id {
color: $black;
}
- .branch-commit {
- width: 30%;
+ .pipeline-date,
+ .pipeline-status {
+ width: 10%;
+ }
- .branch-name {
- max-width: 195px;
- }
+ .pipeline-info,
+ .pipeline-commit,
+ .pipeline-stages,
+ .pipeline-actions {
+ width: 20%;
}
}
}
-.content-list {
-
- &.pipelines,
- &.builds-content-list {
- width: 100%;
- overflow: auto;
+@media (max-width: $screen-md-max) {
+ .content-list {
+ &.pipelines,
+ &.builds-content-list {
+ width: 100%;
+ overflow: auto;
+ }
}
}
+.content-list.pipelines .table-holder {
+ min-height: 300px;
+}
+
.pipeline-holder {
width: 100%;
overflow: auto;
@@ -74,6 +93,10 @@
td {
padding: 10px 8px;
}
+
+ .commit-link {
+ padding: 9px 8px 10px;
+ }
}
tbody {
@@ -91,14 +114,6 @@
}
}
- .ci-status {
-
- svg {
- top: 1px;
- margin-right: 0;
- }
- }
-
a:hover {
text-decoration: none;
}
@@ -109,15 +124,11 @@
float: none;
}
- .api {
- color: $code-color;
- }
-
.branch-commit {
.branch-name {
font-weight: bold;
- max-width: 150px;
+ max-width: 120px;
overflow: hidden;
display: inline-block;
white-space: nowrap;
@@ -129,7 +140,7 @@
height: 14px;
width: 14px;
vertical-align: middle;
- fill: $table-text-gray;
+ fill: $gl-text-color-secondary;
}
.fa {
@@ -143,7 +154,7 @@
.commit-title {
margin-top: 4px;
- max-width: 300px;
+ max-width: 225px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@@ -174,13 +185,16 @@
.stage-cell {
font-size: 0;
+ padding: 10px 4px;
- svg {
- height: 18px;
- width: 18px;
- position: relative;
+ > .stage-container > div > button > span > svg,
+ > .stage-container > button > svg {
+ height: 22px;
+ width: 22px;
+ position: absolute;
+ top: -1px;
+ left: -1px;
z-index: 2;
- vertical-align: middle;
overflow: visible;
}
@@ -189,8 +203,8 @@
position: relative;
margin-right: 6px;
- .tooltip {
- white-space: nowrap;
+ .tooltip-inner {
+ padding: 3px 4px;
}
&:not(:last-child) {
@@ -198,21 +212,17 @@
content: '';
width: 8px;
position: absolute;
- right: -7px;
- bottom: 8px;
+ right: -8px;
+ top: 10px;
border-bottom: 2px solid $border-color;
}
}
-
- a {
- display: block;
- }
}
}
.duration,
.finished-at {
- color: $table-text-gray;
+ color: $gl-text-color-secondary;
margin: 4px 0;
.fa {
@@ -233,7 +243,7 @@
.btn {
margin: 0;
- color: $table-text-gray;
+ color: $gl-text-color-secondary;
}
.cancel-retry-btns {
@@ -246,10 +256,10 @@
.dropdown-toggle,
.dropdown-menu {
- color: $table-text-gray;
+ color: $gl-text-color-secondary;
.fa {
- color: $table-text-gray;
+ color: $gl-text-color-secondary;
font-size: 14px;
}
@@ -283,7 +293,7 @@
.build-link {
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
}
@@ -292,68 +302,170 @@
}
}
+.admin-builds-table {
+ .ci-table td:last-child {
+ min-width: 120px;
+ }
+}
+
// Pipeline visualization
+.pipeline-actions {
+ border-bottom: none;
+}
-.toggle-pipeline-btn {
- background-color: $gray-dark;
+.tab-pane {
+ &.pipelines {
+ .ci-table {
+ min-width: 900px;
+ }
- &.graph-collapsed {
- background-color: $white-light;
+ .content-list.pipelines {
+ overflow: auto;
+ }
+
+ .stage {
+ max-width: 100px;
+ width: 100px;
+ }
+
+ .pipeline-actions {
+ min-width: initial;
+ }
+ }
+
+ &.builds {
+ .ci-table {
+ tr {
+ height: 71px;
+ }
+ }
}
}
+// Pipeline graph
.pipeline-graph {
width: 100%;
- overflow: auto;
+ background-color: $gray-light;
+ padding: $gl-padding;
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
+ overflow: auto;
- &.graph-collapsed {
- max-height: 0;
- padding: 0 16px;
+ .stage-column-list,
+ .builds-container > ul {
+ padding: 0;
}
-}
-.pipeline-visualization {
- position: relative;
+ a {
+ text-decoration: none;
+ color: $gl-text-color-secondary;
+ }
- ul {
- padding: 0;
+ svg {
+ vertical-align: middle;
+ margin-right: 3px;
}
-}
-.stage-column {
- display: inline-block;
- vertical-align: top;
+ .stage-column {
+ display: inline-block;
+ vertical-align: top;
- &:not(:last-child) {
- margin-right: 44px;
- }
+ &:not(:last-child) {
+ margin-right: 44px;
+ }
- &.left-margin {
- &:not(:first-child) {
- margin-left: 44px;
+ &.left-margin {
+ &:not(:first-child) {
+ margin-left: 44px;
- .left-connector {
- &::before {
- content: '';
- position: absolute;
- top: 48%;
- left: -48px;
- border-top: 2px solid $border-color;
- width: 48px;
- height: 1px;
+ .left-connector {
+ &::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -48px;
+ border-top: 2px solid $border-color;
+ width: 48px;
+ height: 1px;
+ }
}
}
}
- }
- &.no-margin {
- margin: 0;
- }
+ &.no-margin {
+ margin: 0;
+ }
- li {
- list-style: none;
+ li {
+ list-style: none;
+ }
+
+ &:last-child {
+ .build {
+ // Remove right connecting horizontal line from first build in last stage
+ &:first-child {
+ &::after {
+ border: none;
+ }
+ }
+ // Remove right curved connectors from all builds in last stage
+ &:not(:first-child) {
+ &::after {
+ border: none;
+ }
+ }
+ // Remove opposite curve
+ .curve {
+ &::before {
+ display: none;
+ }
+ }
+ }
+ }
+
+ &:first-child {
+ .build {
+ // Remove left curved connectors from all builds in first stage
+ &:not(:first-child) {
+ &::before {
+ border: none;
+ }
+ }
+ // Remove opposite curve
+ .curve {
+ &::after {
+ display: none;
+ }
+ }
+ }
+ }
+
+ // Curve first child connecting lines in opposite direction
+ .curve {
+ display: none;
+
+ &::before,
+ &::after {
+ content: '';
+ width: 21px;
+ height: 25px;
+ position: absolute;
+ top: -31px;
+ border-top: 2px solid $border-color;
+ }
+
+ &::after {
+ left: -44px;
+ border-right: 2px solid $border-color;
+ border-radius: 0 20px;
+ }
+
+ &::before {
+ right: -44px;
+ border-left: 2px solid $border-color;
+ border-radius: 20px 0 0;
+ }
+ }
}
.stage-name {
@@ -366,166 +478,114 @@
}
.build {
- border: 1px solid $border-color;
position: relative;
- padding: 7px 10px 8px;
- border-radius: 30px;
width: 186px;
margin-bottom: 10px;
+ white-space: normal;
+ color: $gl-text-color-secondary;
+
+ // Action Icons in big pipeline-graph nodes
+ > .ci-action-icon-container .ci-action-icon-wrapper {
+ i {
+ color: $border-color;
+ border-radius: 100%;
+ border: 1px solid $border-color;
+ padding: 5px 6px;
+ font-size: 13px;
+ background: $white-light;
+ height: 30px;
+ width: 30px;
- &:hover {
- background-color: $gray-lighter;
- }
-
- &.playable {
-
- svg {
- height: 13px;
- width: 20px;
- position: relative;
- top: 1px;
-
- path {
- fill: $layout-link-gray;
- }
- }
- }
-
- .build-content {
- display: -ms-flexbox;
- display: -webkit-flex;
- display: flex;
- width: 164px;
-
- .ci-status-icon {
- svg {
- height: 20px;
- width: 20px;
+ &::before {
+ position: relative;
+ top: 3px;
+ left: 3px;
}
- }
-
- .tooltip {
- white-space: nowrap;
- .tooltip-inner {
- overflow: hidden;
- text-overflow: ellipsis;
+ &:hover {
+ color: $gl-text-color;
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-bg;
}
}
- .ci-status-text {
- width: 135px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: middle;
- display: inline-block;
- position: relative;
- top: -1px;
- }
-
- a {
- color: $gl-text-color-light;
- text-decoration: none;
+ .ci-play-icon {
+ padding: 5px 5px 5px 7px;
}
+ }
- .dropdown-menu-toggle {
- background-color: transparent;
- border: none;
- width: auto;
- padding: 0;
- color: $gl-text-color-light;
- flex-grow: 1;
-
- .ci-status-text {
- max-width: 112px;
- width: auto;
- }
- }
+ > .ci-action-icon-container {
+ position: absolute;
+ right: 5px;
+ top: 5px;
+ }
- .grouped-pipeline-dropdown {
- padding: 0;
- width: 186px;
- left: auto;
- right: -197px;
- top: -9px;
+ .ci-status-icon svg {
+ height: 20px;
+ width: 20px;
+ }
- ul {
- max-height: 245px;
- overflow: auto;
+ .dropdown-menu-toggle {
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ color: $gl-text-color-secondary;
- li:first-child {
- padding-top: 8px;
- }
+ &:focus {
+ outline: none;
+ }
- li:last-child {
- padding-bottom: 8px;
- }
- }
+ &:hover {
+ color: $gl-text-color;
- a {
+ .dropdown-counter-badge {
color: $gl-text-color;
- padding: 7px 8px 8px;
-
- &:hover {
- background-color: $blue-light-transparent;
- border-radius: 3px;
-
- .ci-status-text {
- text-decoration: none;
- }
- }
- }
-
- svg {
- width: 14px;
- height: 14px;
}
+ }
+ }
- .ci-status-text {
- width: 112px;
- }
+ > .build-content {
+ display: inline-block;
+ padding: 8px 10px 9px;
+ width: 100%;
+ border: 1px solid $border-color;
+ border-radius: 30px;
+ background-color: $white-light;
- .arrow {
- &::before,
- &::after {
- content: '';
- display: inline-block;
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- top: 18px;
- }
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-border;
+ color: $gl-text-color;
+ }
+ }
- &::before {
- left: -5px;
- margin-top: -6px;
- border-width: 7px 5px 7px 0;
- border-right-color: $border-color;
- }
- &::after {
- left: -4px;
- margin-top: -9px;
- border-width: 10px 7px 10px 0;
- border-right-color: $white-light;
- }
- }
+ .arrow {
+ &::before,
+ &::after {
+ content: '';
+ display: inline-block;
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: 18px;
}
- .badge {
- background-color: $gray-darker;
- color: $gl-text-color-light;
- font-weight: normal;
- margin-left: $btn-xs-side-margin;
+ &::before {
+ left: -5px;
+ margin-top: -6px;
+ border-width: 7px 5px 7px 0;
+ border-right-color: $border-color;
}
- }
- svg {
- vertical-align: middle;
- margin-right: 5px;
+ &::after {
+ left: -4px;
+ margin-top: -9px;
+ border-width: 10px 7px 10px 0;
+ border-right-color: $white-light;
+ }
}
// Connect first build in each stage with right horizontal line
@@ -581,121 +641,302 @@
}
}
}
+}
- &:last-child {
- .build {
- // Remove right connecting horizontal line from first build in last stage
- &:first-child {
- &::after {
- border: none;
- }
- }
- // Remove right curved connectors from all builds in last stage
- &:not(:first-child) {
- &::after {
- border: none;
- }
- }
- // Remove opposite curve
- .curve {
- &::before {
- display: none;
- }
- }
+// Triggers the dropdown in the big pipeline graph
+.dropdown-counter-badge {
+ color: $border-color;
+ font-weight: 100;
+ font-size: 15px;
+ position: absolute;
+ right: 5px;
+ top: 8px;
+}
+
+.ci-status-text {
+ max-width: 110px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: bottom;
+ display: inline-block;
+ position: relative;
+ font-weight: 200;
+}
+
+// Dropdown button in mini pipeline graph
+.mini-pipeline-graph-dropdown-toggle {
+ border-radius: 100px;
+ background-color: $white-light;
+ border-width: 1px;
+ border-style: solid;
+ width: 22px;
+ height: 22px;
+ margin: 0;
+ padding: 0;
+ transition: all 0.2s linear;
+ position: relative;
+
+ > .fa.fa-caret-down {
+ position: absolute;
+ left: 20px;
+ top: 5px;
+ display: inline-block;
+ visibility: hidden;
+ opacity: 0;
+ color: inherit;
+ font-size: 12px;
+ transition: visibility 0.1s, opacity 0.1s linear;
+ }
+
+ &:active,
+ &:focus,
+ &:hover {
+ outline: none;
+ width: 35px;
+
+ .fa.fa-caret-down {
+ visibility: visible;
+ opacity: 1;
}
}
- &:first-child {
- .build {
- // Remove left curved connectors from all builds in first stage
- &:not(:first-child) {
- &::before {
- border: none;
- }
- }
- // Remove opposite curve
- .curve {
- &::after {
- display: none;
- }
- }
+ // Dropdown button animation in mini pipeline graph
+ &.ci-status-icon-success {
+ border-color: $gl-success;
+ color: $gl-success;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($gl-success, 0.1);
+ border-color: $gl-success;
}
}
- // Curve first child connecting lines in opposite direction
- .curve {
- display: none;
+ &.ci-status-icon-failed {
+ border-color: $gl-danger;
+ color: $gl-danger;
- &::before,
- &::after {
- content: '';
- width: 21px;
- height: 25px;
- position: absolute;
- top: -32px;
- border-top: 2px solid $border-color;
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($gl-danger, 0.1);
+ border-color: $gl-danger;
}
+ }
- &::after {
- left: -44px;
- border-right: 2px solid $border-color;
- border-radius: 0 20px;
+ &.ci-status-icon-pending,
+ &.ci-status-icon-success_with_warnings {
+ border-color: $gl-warning;
+ color: $gl-warning;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($gl-warning, 0.1);
+ border-color: $gl-warning;
}
+ }
- &::before {
- right: -44px;
- border-left: 2px solid $border-color;
- border-radius: 20px 0 0;
+ &.ci-status-icon-running {
+ border-color: $blue-normal;
+ color: $blue-normal;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($blue-normal, 0.1);
+ border-color: $blue-normal;
}
}
-}
-.pipeline-actions {
- border-bottom: none;
-}
+ &.ci-status-icon-canceled,
+ &.ci-status-icon-disabled,
+ &.ci-status-icon-not-found,
+ &.ci-status-icon-manual {
+ border-color: $gl-text-color;
+ color: $gl-text-color;
-.toggle-pipeline-btn {
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($gl-text-color, 0.1);
+ border-color: $gl-text-color;
+ }
+ }
- .fa {
- color: $dropdown-header-color;
+ &.ci-status-icon-created,
+ &.ci-status-icon-skipped {
+ border-color: $gray-darkest;
+ color: $gray-darkest;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: rgba($gray-darkest, 0.1);
+ border-color: $gray-darkest;
+ }
}
}
-.tab-pane {
+// dropdown content for big and mini pipeline
+.big-pipeline-graph-dropdown-menu,
+.mini-pipeline-graph-dropdown-menu {
+ width: 195px;
+ max-width: 195px;
- &.pipelines {
+ li {
+ padding: 2px 3px;
+ }
- .ci-table {
- min-width: 900px;
+ .scrollable-menu {
+ max-height: 245px;
+ overflow: auto;
+ }
+
+ // Loading icon
+ .builds-dropdown-loading {
+ margin: 0 auto;
+ width: 20px;
+ }
+
+ // Action icon on the right
+ a.ci-action-icon-wrapper {
+ color: $action-icon-color;
+ border: 1px solid $action-icon-color;
+ border-radius: 20px;
+ width: 22px;
+ height: 22px;
+ padding: 2px 0 0 5px;
+ cursor: pointer;
+ float: right;
+ margin: -26px 9px 0 0;
+ font-size: 12px;
+ background-color: $white-light;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ color: $gl-text-color;
+ background-color: $stage-hover-bg;
+ border: 1px solid transparent;
}
+ }
- .content-list.pipelines {
- overflow: auto;
+ // link to the build
+ .mini-pipeline-graph-dropdown-item {
+ padding: 3px 7px 4px;
+ clear: both;
+ font-weight: normal;
+ line-height: 1.428571429;
+ white-space: nowrap;
+ margin: 0 5px;
+ border-radius: 3px;
+
+ // build name
+ .ci-build-text {
+ font-weight: 200;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ width: 90px;
+ color: $gl-text-color-secondary;
+ margin-left: 2px;
+ display: inline-block;
+ top: 1px;
+ vertical-align: text-bottom;
+ position: relative;
}
- .stage {
- max-width: 100px;
- width: 100px;
+ // status icon on the left
+ .ci-status-icon {
+ top: 3px;
+ position: relative;
+
+ > svg {
+ overflow: visible;
+ width: 18px;
+ height: 18px;
+ }
}
- .pipeline-actions {
- min-width: initial;
+ &:hover,
+ &:focus {
+ outline: none;
+ text-decoration: none;
+ color: $gl-text-color;
+ background-color: $stage-hover-bg;
}
}
+}
- &.builds {
+// Dropdown in the big pipeline graph
+.big-pipeline-graph-dropdown-menu {
+ width: 195px;
+ min-width: 195px;
+ left: auto;
+ right: -195px;
+ top: -4px;
+ box-shadow: 0 1px 5px $black-transparent;
- .ci-table {
- tr {
- height: 71px;
- }
+ .mini-pipeline-graph-dropdown-item {
+ .ci-status-icon {
+ top: -1px;
}
}
}
-.ci-status-icon-created {
+/**
+ * Top arrow in the dropdown in the mini pipeline graph
+ */
+.mini-pipeline-graph-dropdown-menu {
+ .arrow-up {
+ &::before,
+ &::after {
+ content: '';
+ display: inline-block;
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: -6px;
+ left: 2px;
+ border-width: 0 5px 6px;
+ }
- svg {
- fill: $gray-darkest;
+ &::before {
+ border-width: 0 5px 5px;
+ border-bottom-color: $border-color;
+ }
+
+ &::after {
+ margin-top: 1px;
+ border-bottom-color: $white-light;
+ }
+ }
+}
+
+/**
+ * Terminal
+ */
+.terminal-icon {
+ margin-left: 3px;
+}
+
+.terminal-container {
+ .content-block {
+ border-bottom: none;
+ }
+
+ #terminal {
+ margin-top: 10px;
+ min-height: 450px;
+ box-sizing: border-box;
+
+ > div {
+ min-height: 450px;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 6fab97a71aa..722b3006f7c 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -60,8 +60,8 @@
.account-well {
padding: 10px;
- background-color: $help-well-bg;
- border: 1px solid $help-well-border;
+ background-color: $gray-light;
+ border: 1px solid $border-color;
border-radius: $border-radius-base;
ul {
@@ -136,7 +136,7 @@
.provider-btn-group {
display: inline-block;
margin-right: 10px;
- border: 1px solid $provider-btn-group-border;
+ border: 1px solid $border-color;
border-radius: 3px;
&:last-child {
@@ -147,7 +147,7 @@
.provider-btn-image {
display: inline-block;
padding: 5px 10px;
- border-right: 1px solid $provider-btn-group-border;
+ border-right: 1px solid $border-color;
> img {
width: 20px;
@@ -180,7 +180,7 @@
.modal-dialog {
width: 380px;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
width: auto;
}
@@ -198,7 +198,7 @@
}
.personal-access-tokens-never-expires-label {
- color: $personal-access-tokens-disabled-label-color;
+ color: $note-disabled-comment-color;
}
.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
@@ -216,8 +216,8 @@
}
}
-.user-profile {
+.user-profile {
.cover-controls a {
margin-left: 5px;
}
@@ -231,8 +231,11 @@
}
}
- @media (max-width: $screen-xs-max) {
+ .user-profile-nav {
+ font-size: 0;
+ }
+ @media (max-width: $screen-xs-max) {
.cover-block {
padding-top: 20px;
}
@@ -253,6 +256,12 @@
}
}
}
+
+ .user-profile-nav {
+ a {
+ margin-right: 0;
+ }
+ }
}
}
@@ -261,4 +270,14 @@ table.u2f-registrations {
td:not(:last-child) {
border-right: solid 1px transparent;
}
-} \ No newline at end of file
+}
+
+.oauth-application-show {
+ .scope-name {
+ font-weight: 600;
+ }
+
+ .scopes-list {
+ padding-left: 18px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index f8da0983b77..100ace41f2a 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -22,8 +22,8 @@
background: $theme-graphite;
}
- &.ui_gray {
- background: $theme-gray;
+ &.ui_black {
+ background: $theme-black;
}
&.ui_green {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f7d54564530..cd0839e58ea 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -6,33 +6,27 @@
}
}
-.no-ssh-key-message,
-.project-limit-message {
- background-color: #f28d35;
- margin-bottom: 0;
-}
-
.new_project,
.edit-project {
- fieldset {
-
- &.features {
+ .sharing-and-permissions {
+ .header {
+ padding-top: $gl-vert-padding;
+ }
- .label-light {
- margin-bottom: 0;
- }
+ .label-light {
+ margin-bottom: 0;
+ }
- .help-block {
- margin-top: 0;
- }
+ .help-block {
+ margin-top: 0;
}
.form-group {
margin-bottom: 5px;
}
- &> .form-group {
+ & > .form-group {
padding-left: 0;
}
}
@@ -76,17 +70,18 @@
&.static-namespace {
height: 35px;
border-radius: 3px;
- border: 1px solid #e5e5e5;
+ border: 1px solid $border-color;
}
- &+ .select2 a {
+ & + .select2 a {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}
-.project-home-panel {
+.project-home-panel,
+.group-home-panel {
padding-top: 24px;
padding-bottom: 24px;
@@ -94,17 +89,18 @@
border-bottom: 1px solid $border-color;
}
- .project-avatar {
+ .project-avatar,
+ .group-avatar {
float: none;
margin: 0 auto;
- border: none;
&.identicon {
border-radius: 50%;
}
}
- .project-title {
+ .project-title,
+ .group-title {
margin-top: 10px;
margin-bottom: 10px;
font-size: 24px;
@@ -118,10 +114,11 @@
}
}
- .project-home-desc {
+ .project-home-desc,
+ .group-home-desc {
margin-left: auto;
margin-right: auto;
- margin-bottom: 15px;
+ margin-bottom: 0;
max-width: 700px;
> p {
@@ -141,13 +138,20 @@
}
}
-.project-repo-buttons {
- font-size: 0;
+.nav > .project-repo-buttons {
+ margin-top: 0;
+}
+.project-repo-buttons,
+.group-buttons {
.btn {
@include btn-gray;
padding: 3px 10px;
+ &:last-child {
+ margin-left: 0;
+ }
+
.fa {
color: $layout-link-gray;
}
@@ -168,22 +172,33 @@
}
}
- .project-repo-btn-group,
- .notification-dropdown,
- .project-dropdown {
- margin-left: 10px;
+ .project-action-button {
+ margin: 15px 5px 0;
+ vertical-align: top;
+ }
+
+ .notification-dropdown .dropdown-menu {
+ @extend .dropdown-menu-align-right;
+ }
+
+ .download-button {
+ @media (max-width: $screen-md-max) {
+ margin-left: 0;
+ }
}
.count-buttons {
display: inline-block;
vertical-align: top;
+ margin-top: 15px;
}
.project-clone-holder {
display: inline-block;
+ margin: 15px 5px 0 0;
input {
- height: 29px;
+ height: 27px;
}
}
@@ -205,7 +220,7 @@
left: 0;
margin-top: -6px;
border-width: 7px 5px 7px 0;
- border-right-color: #dce0e5;
+ border-right-color: $count-arrow-border;
pointer-events: none;
}
@@ -220,7 +235,7 @@
left: 1px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
- border-right-color: #fff;
+ border-right-color: $white-light;
pointer-events: none;
}
}
@@ -228,7 +243,7 @@
.count {
@include btn-gray;
display: inline-block;
- background: white;
+ background: $white-light;
border-radius: 2px;
border-width: 1px;
border-style: solid;
@@ -237,7 +252,7 @@
line-height: 13px;
padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
- padding: 7px 14px;
+ padding: 6px 14px;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
@@ -250,7 +265,7 @@
}
&:hover {
- background: #fff;
+ background: $white-light;
}
}
}
@@ -277,12 +292,12 @@
.option-title {
font-weight: normal;
display: inline-block;
- color: $gl-gray-dark;
+ color: $gl-text-color;
}
.option-descr {
margin-left: 29px;
- color: #54565b;
+ color: $project-option-descr-color;
}
}
}
@@ -290,7 +305,7 @@
.save-project-loader {
margin-top: 50px;
margin-bottom: 50px;
- color: #555;
+ color: $save-project-loader-color;
}
.transfer-project .select2-container {
@@ -316,7 +331,7 @@
a.deploy-project-label {
padding: 5px;
margin-right: 5px;
- color: $gl-gray;
+ color: $gl-text-color;
background-color: $row-hover;
&:hover {
@@ -353,11 +368,11 @@ a.deploy-project-label {
> li + li::before {
padding: 0 3px;
- color: #999;
+ color: $project-breadcrumb-color;
}
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
.dropdown-menu {
@@ -396,13 +411,13 @@ a.deploy-project-label {
width: 100px;
height: 100px;
background-color: $gray-light;
- border: 1px solid $gray-dark;
+ border: 1px solid $white-normal;
margin: 0 auto;
border-radius: 50%;
i {
font-size: 100px;
- color: $gray-dark;
+ color: $white-normal;
}
}
@@ -411,7 +426,7 @@ a.deploy-project-label {
width: 100%;
height: 100%;
padding-top: $gl-padding;
- color: $gl-gray;
+ color: $gl-text-color;
.caption {
min-height: 30px;
@@ -458,8 +473,23 @@ a.deploy-project-label {
}
}
+.page-sidebar-pinned {
+ .project-stats .nav > li.right {
+ @media (min-width: $screen-lg-min) {
+ float: none;
+ }
+ }
+
+ .download-button {
+ @media (min-width: $screen-lg-min) {
+ margin-left: 0;
+ }
+ }
+}
+
.project-stats {
font-size: 0;
+ text-align: center;
border-bottom: 1px solid $border-color;
.nav {
@@ -474,12 +504,12 @@ a.deploy-project-label {
margin-right: $gl-padding;
}
- &.project-repo-buttons-right {
- margin-top: 10px;
+ &.right {
+ vertical-align: top;
+ margin-top: 0;
- @media (min-width: $screen-md-min) {
+ @media (min-width: $screen-lg-min) {
float: right;
- margin-top: 0;
}
}
}
@@ -487,7 +517,7 @@ a.deploy-project-label {
.nav > li > a {
padding: 0;
background-color: transparent;
- font-size: 15px;
+ font-size: 14px;
line-height: 29px;
color: $notes-light-color;
@@ -498,7 +528,7 @@ a.deploy-project-label {
}
li.missing {
- border: 1px dashed $border-gray-light;
+ border: 1px dashed $border-gray-normal;
border-radius: $border-radius-default;
a {
@@ -515,20 +545,20 @@ a.deploy-project-label {
}
pre.light-well {
- border-color: #f1f1f1;
+ border-color: $well-light-border;
}
.git-empty {
margin: 0 7px 7px;
h5 {
- color: #5c5d5e;
+ color: $gl-text-color;
}
.light-well {
border-radius: 2px;
- color: #5b6169;
+ color: $well-light-text-color;
font-size: 13px;
line-height: 1.6em;
}
@@ -553,15 +583,25 @@ pre.light-well {
@include basic-list;
.project-row {
- border-color: $table-border-color;
+ border-color: $white-normal;
.project-full-name {
@include str-truncated;
+
+ @media (max-width: $screen-xs-max) {
+ max-width: 50%;
+ }
}
.controls {
line-height: $list-text-height;
+ .badge {
+ @media (max-width: $screen-xs-max) {
+ display: none;
+ }
+ }
+
a:hover {
text-decoration: none;
}
@@ -575,6 +615,12 @@ pre.light-well {
top: 2px;
}
}
+
+ .description p {
+ @media (max-width: $screen-xs-max) {
+ max-width: 50%;
+ }
+ }
}
.bottom {
@@ -588,7 +634,6 @@ pre.light-well {
margin: 0;
}
-
.activity-filter-block {
.controls {
padding-bottom: 7px;
@@ -597,6 +642,12 @@ pre.light-well {
}
}
+.commits-search-form {
+ .input-short {
+ min-width: 200px;
+ }
+}
+
.project-last-commit {
@media (min-width: $screen-sm-min) {
margin-top: $gl-padding;
@@ -605,7 +656,7 @@ pre.light-well {
&.container-fluid {
padding-top: 12px;
padding-bottom: 12px;
- background-color: $background-color;
+ background-color: $gray-light;
border: 1px solid $border-color;
border-right-width: 0;
border-left-width: 0;
@@ -627,7 +678,7 @@ pre.light-well {
}
.commit-row-message {
- color: $gl-gray;
+ color: $gl-text-color;
}
.commit_short_id {
@@ -682,7 +733,7 @@ pre.light-well {
.form-control {
@extend .monospace;
- background: #fff;
+ background: $white-light;
font-size: 14px;
margin-left: -1px;
cursor: auto;
@@ -692,17 +743,17 @@ pre.light-well {
.cannot-be-merged,
.cannot-be-merged:hover {
- color: #e62958;
+ color: $error-exclamation-point;
margin-top: 2px;
}
.private-forks-notice .private-fork-icon {
i:nth-child(1) {
- color: #2aa056;
+ color: $project-private-forks-notice-odd;
}
i:nth-child(2) {
- color: #fff;
+ color: $white-light;
}
}
@@ -715,7 +766,7 @@ pre.light-well {
.protected-branches-list {
a {
- color: $gl-gray;
+ color: $gl-text-color;
&:hover {
color: $gl-link-color;
@@ -775,7 +826,31 @@ pre.light-well {
.compare-form-group {
.dropdown-menu {
- width: 300px;
+ width: 100%;
+
+ @media (min-width: $screen-sm-min) {
+ width: 300px;
+ }
+ }
+
+ + .compare-ellipsis {
+ width: 100%;
+ vertical-align: middle;
+ text-align: center;
+ margin-top: -20px;
+
+ @media (min-width: $screen-sm-min) {
+ margin-top: 0;
+ width: auto;
+ }
+ }
+
+ .inline-input-group {
+ width: 100%;
+
+ @media (min-width: $screen-sm-min) {
+ width: 250px;
+ }
}
}
@@ -830,10 +905,18 @@ pre.light-well {
}
}
-.project-feature-nested {
+.project-feature {
+ padding-top: 10px;
+
@media (min-width: $screen-sm-min) {
padding-left: 45px;
}
+
+ &.nested {
+ @media (min-width: $screen-sm-min) {
+ padding-left: 90px;
+ }
+ }
}
.project-repo-select {
@@ -842,3 +925,31 @@ pre.light-well {
pointer-events: none;
}
}
+
+.variables-table {
+ table-layout: fixed;
+
+ .variable-key {
+ width: 30%;
+ }
+}
+
+.services-installation-info .row {
+ margin-bottom: 10px;
+}
+
+.service-installation {
+ padding: 32px;
+ margin: 32px;
+ border-radius: 3px;
+ background-color: $white-light;
+
+ h3 {
+ margin-top: 0;
+ }
+
+ hr {
+ margin: 32px 0;
+ border-color: $border-color;
+ }
+}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 7b3878c91df..9b6ff237557 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -1,27 +1,27 @@
.runner-state {
padding: 6px 12px;
margin-right: 10px;
- color: #fff;
+ color: $white-light;
&.runner-state-shared {
- background: #32b186;
+ background: $runner-state-shared-bg;
}
&.runner-state-specific {
- background: #3498db;
+ background: $runner-state-specific-bg;
}
}
.runner-status-online {
- color: green;
+ color: $runner-status-online-color;
}
.runner-status-offline {
- color: gray;
+ color: $runner-status-offline-color;
}
.runner-status-paused {
- color: red;
+ color: $runner-status-paused-color;
}
.runner {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index b4761df3f23..88ea92c5afb 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -8,6 +8,23 @@
border-bottom: none;
}
}
+
+ .blob-result {
+ margin: 5px 0;
+ }
+}
+
+.search form:hover,
+.file-finder-input:hover,
+.issuable-search-form:hover,
+.search-text-input:hover,
+.form-control:hover {
+ border-color: lighten($dropdown-input-focus-border, 20%);
+ box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
+}
+
+input[type="checkbox"]:hover {
+ box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), 0 0 0 1px lighten($search-input-focus-shadow-color, 20%);
}
.search {
@@ -21,6 +38,11 @@
padding: 4px;
width: $search-input-width;
line-height: 24px;
+
+ &:hover {
+ border-color: lighten($dropdown-input-focus-border, 20%);
+ box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
+ }
}
.location-text {
@@ -28,10 +50,9 @@
}
.search-input {
- padding-right: 20px;
border: none;
font-size: 14px;
- padding: 0;
+ padding: 0 20px 0 0;
margin-left: 5px;
line-height: 25px;
width: 98%;
@@ -43,9 +64,9 @@
border-radius: $border-radius-default;
font-size: 14px;
font-style: normal;
- color: $location-badge-color;
+ color: $note-disabled-comment-color;
display: inline-block;
- background-color: $location-badge-bg;
+ background-color: $gray-normal;
vertical-align: top;
cursor: default;
}
@@ -132,7 +153,7 @@
.search-input-wrap {
i {
- color: $location-icon-active-color;
+ color: $layout-link-gray;
}
}
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 2e8f356298d..a28a87ed4f8 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -1,5 +1,5 @@
.settings-list-icon {
- color: $gl-placeholder-color;
+ color: $gl-text-color-secondary;
font-size: $settings-icon-size;
line-height: 42px;
}
@@ -20,3 +20,7 @@
.danger-title {
color: $gl-danger;
}
+
+.service-settings .control-label {
+ padding-top: 0;
+}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index 69288b31cc4..dfa4d033fb8 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -1,16 +1,16 @@
.tint-box {
- background: #f3f3f3;
+ background: $stat-graph-common-bg;
position: relative;
margin-bottom: 10px;
}
.area {
- fill: #1db34f;
+ fill: $stat-graph-area-fill;
fill-opacity: 0.5;
}
.axis {
- fill: #aaa;
+ fill: $stat-graph-axis-fill;
font-size: 10px;
}
@@ -37,26 +37,26 @@
@include make-md-column(6);
margin-top: 10px;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
width: 100%;
}
}
.person .spark {
display: block;
- background: #f3f3f3;
+ background: $stat-graph-common-bg;
width: 100%;
}
.person .area-contributor {
- fill: #f17f49;
+ fill: $stat-graph-orange-fill;
}
}
.selection rect {
- fill: #333;
+ fill: $stat-graph-selection-fill;
fill-opacity: 0.1;
- stroke: #333;
+ stroke: $stat-graph-selection-stroke;
stroke-width: 1px;
stroke-opacity: 0.4;
shape-rendering: crispedges;
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 92997eae8b9..6f31d4ed789 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,8 +1,8 @@
.container-fluid {
.ci-status {
- padding: 2px 7px;
+ padding: 2px 7px 4px;
margin-right: 10px;
- border: 1px solid #eee;
+ border: 1px solid $gray-darker;
white-space: nowrap;
border-radius: 4px;
@@ -11,80 +11,121 @@
text-decoration: none;
}
- &.ci-failed {
+ svg {
+ height: 13px;
+ width: 13px;
+ position: relative;
+ top: 2px;
+ overflow: visible;
+ }
+
+ &.ci-failed,
+ &.ci-failed_with_warnings {
color: $gl-danger;
border-color: $gl-danger;
+
+ &:not(span):hover {
+ background-color: rgba($gl-danger, .07);
+ }
+
+ svg {
+ fill: $gl-danger;
+ }
}
&.ci-success,
&.ci-success_with_warnings {
color: $gl-success;
border-color: $gl-success;
+
+ &:not(span):hover {
+ background-color: rgba($gl-success, .07);
+ }
+
+ svg {
+ fill: $gl-success;
+ }
}
&.ci-info {
color: $gl-info;
border-color: $gl-info;
+
+ &:not(span):hover {
+ background-color: rgba($gl-info, .07);
+ }
+
+ svg {
+ fill: $gl-info;
+ }
}
&.ci-canceled,
- &.ci-skipped,
&.ci-disabled {
- color: $gl-gray;
- border-color: $gl-gray;
+ color: $gl-text-color;
+ border-color: $gl-text-color;
+
+ &:not(span):hover {
+ background-color: rgba($gl-text-color, .07);
+ }
+
+ svg {
+ fill: $gl-text-color;
+ }
}
&.ci-pending {
color: $gl-warning;
border-color: $gl-warning;
+
+ &:not(span):hover {
+ background-color: rgba($gl-warning, .07);
+ }
+
+ svg {
+ fill: $gl-warning;
+ }
}
&.ci-running {
color: $blue-normal;
border-color: $blue-normal;
- }
- &.ci-created {
- color: $table-text-gray;
- border-color: $table-text-gray;
+ &:not(span):hover {
+ background-color: rgba($blue-normal, .07);
+ }
svg {
- fill: $table-text-gray;
+ fill: $blue-normal;
}
}
- svg {
- height: 13px;
- width: 13px;
- position: relative;
- top: 1px;
- margin: 0 3px;
- overflow: visible;
- }
- }
+ &.ci-created,
+ &.ci-skipped {
+ color: $gl-text-color-secondary;
+ border-color: $gl-text-color-secondary;
- .ci-status-icon-success {
- color: $gl-success;
- }
+ &:not(span):hover {
+ background-color: rgba($gl-text-color-secondary, .07);
+ }
- .ci-status-icon-failed {
- color: $gl-danger;
- }
+ svg {
+ fill: $gl-text-color-secondary;
+ }
+ }
- .ci-status-icon-pending,
- .ci-status-icon-success_with_warning {
- color: $gl-warning;
- }
+ &.ci-manual {
+ color: $gl-text-color;
+ border-color: $gl-text-color;
- .ci-status-icon-running {
- color: $blue-normal;
- }
+ &:not(span):hover {
+ background-color: rgba($gl-text-color, .07);
+ }
- .ci-status-icon-canceled,
- .ci-status-icon-disabled,
- .ci-status-icon-not-found,
- .ci-status-icon-skipped {
- color: $gl-gray;
+ svg {
+ fill: $gl-text-color;
+ }
+ }
}
}
@@ -95,3 +136,9 @@
left: 5px;
}
}
+
+.ci-status-link {
+ svg {
+ overflow: visible;
+ }
+}
diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss
deleted file mode 100644
index 24ebd3e7cfa..00000000000
--- a/app/assets/stylesheets/pages/tags.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.tag-buttons {
- line-height: 40px;
-
- .btn:not(.dropdown-toggle) {
- margin-left: 10px;
- }
-}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index b3aef2fdd32..01675acc62e 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -11,7 +11,7 @@
background: $todo-alert-blue;
margin-left: -17px;
font-size: 11px;
- color: white;
+ color: $white-light;
padding: 3px;
padding-top: 1px;
padding-bottom: 1px;
@@ -81,7 +81,7 @@
word-wrap: break-word;
.md {
- color: #7f8fa4;
+ color: $gl-grayish-blue;
font-size: $gl-font-size;
.label {
@@ -90,7 +90,7 @@
}
p {
- color: #5c5d5e;
+ color: $gl-text-color;
}
}
@@ -102,7 +102,7 @@
border: none;
background: $gray-light;
border-radius: 0;
- color: #777;
+ color: $todo-body-pre-color;
margin: 0 20px;
overflow: hidden;
}
@@ -146,7 +146,7 @@
.todo-body {
margin: 0;
- border-left: 2px solid #ddd;
+ border-left: 2px solid $todo-body-border;
padding-left: 10px;
}
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 2b836fa1f4a..4cce1c363eb 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -14,14 +14,15 @@
.add-to-tree {
vertical-align: top;
+ padding: 6px 10px;
}
.tree-table {
margin-bottom: 0;
tr {
- border-bottom: 1px solid $table-border-gray;
- border-top: 1px solid $table-border-gray;
+ border-bottom: 1px solid $white-normal;
+ border-top: 1px solid $white-normal;
td,
th {
@@ -31,7 +32,7 @@
.last-commit {
@include str-truncated(506px);
- @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) {
+ @media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
@include str-truncated(450px);
}
@@ -39,7 +40,7 @@
.commit-history-link-spacer {
margin: 0 10px;
- color: $table-border-color;
+ color: $white-normal;
}
&:hover {
@@ -53,7 +54,7 @@
&.selected {
td {
- background: $gray-dark;
+ background: $white-normal;
border-top: 1px solid $border-gray-dark;
border-bottom: 1px solid $border-gray-dark;
}
@@ -77,7 +78,7 @@
i,
a {
- color: $gl-dark-link-color;
+ color: $gl-text-color;
}
img {
@@ -103,21 +104,21 @@
padding-right: 8px;
.commit-author-name {
- color: $gl-gray;
+ color: $gl-text-color;
}
}
.tree-time-ago {
min-width: 135px;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
}
.tree-commit {
max-width: 320px;
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
.tree-commit-link {
- color: $gl-gray-light;
+ color: $gl-text-color-secondary;
&:hover {
text-decoration: underline;
@@ -133,21 +134,18 @@
.blob-commit-info {
list-style: none;
- padding: $gl-padding;
- background: $background-color;
+ background: $gray-light;
+ padding: 6px 0;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
- .commit {
- padding-top: 0;
- padding-bottom: 0;
+ .table-list-cell {
+ border-bottom: none;
+ }
- .commit-row-title {
- .commit-row-message {
- font-weight: normal;
- }
- }
+ .commit-actions {
+ width: 200px;
}
}
@@ -172,7 +170,7 @@
position: relative;
z-index: 2;
- .download-button {
+ .project-action-button {
margin-left: $btn-side-margin;
}
}
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index e73cecc92be..8c87bc3cafd 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -7,11 +7,11 @@
.example {
&::before {
content: "Example";
- color: #bbb;
+ color: $ui-dev-kit-example-color;
}
padding: 15px;
- border: 1px dashed #ddd;
+ border: 1px dashed $ui-dev-kit-example-border;
margin-bottom: 15px;
}
}
diff --git a/app/assets/stylesheets/pages/votes.scss b/app/assets/stylesheets/pages/votes.scss
deleted file mode 100644
index dc9a7d71e8b..00000000000
--- a/app/assets/stylesheets/pages/votes.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.votes-inline {
- display: inline-block;
- margin: 0 8px;
-}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index dfaeba41cf6..d5783e14b21 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -4,3 +4,128 @@
margin-right: auto;
padding-right: 7px;
}
+
+.wiki-page-header {
+ @extend .top-area;
+ position: relative;
+
+ .wiki-page-title {
+ margin: 0;
+ font-size: 22px;
+ }
+
+ .wiki-last-edit-by {
+ color: $gl-text-color-secondary;
+
+ strong {
+ color: $gl-text-color;
+ }
+ }
+
+ .light {
+ font-weight: normal;
+ color: $gl-text-color-secondary;
+ }
+
+ .git-access-header {
+ padding: 16px 40px 11px 0;
+ line-height: 28px;
+ font-size: 18px;
+ }
+
+ .git-clone-holder {
+ width: 100%;
+ padding-bottom: 40px;
+ }
+
+ button.sidebar-toggle {
+ position: absolute;
+ right: 0;
+ top: 11px;
+ display: block;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ &.has-sidebar-toggle {
+ padding-right: 40px;
+ }
+
+ .git-clone-holder {
+ width: 480px;
+ }
+
+ .nav-controls {
+ width: auto;
+ min-width: 50%;
+ white-space: nowrap;
+ }
+ }
+
+ @media (min-width: $screen-md-min) {
+ &.has-sidebar-toggle {
+ padding-right: 0;
+ }
+
+ button.sidebar-toggle {
+ display: none;
+ }
+ }
+}
+
+.wiki-git-access {
+ margin: $gl-padding 0;
+
+ h3 {
+ font-size: 22px;
+ font-weight: normal;
+ margin-top: 1.4em;
+ }
+}
+
+.right-sidebar.wiki-sidebar {
+ padding: $gl-padding 0;
+
+ &.right-sidebar-collapsed {
+ display: none;
+ }
+
+ .blocks-container {
+ padding: 0 $gl-padding;
+ }
+
+ .block {
+ width: 100%;
+ }
+
+ a {
+ color: $layout-link-gray;
+
+ &:hover,
+ &.active {
+ color: $black;
+ }
+ }
+
+ .active > a {
+ color: $black;
+ }
+
+ ul.wiki-pages,
+ ul.wiki-pages li {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ ul.wiki-pages li {
+ margin: 5px 0 10px;
+ }
+
+ .wiki-sidebar-header {
+ padding: 0 $gl-padding $gl-padding;
+
+ .gutter-toggle {
+ margin-top: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 3fa7fa3d7e3..b085c56390d 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -18,7 +18,267 @@
$l-blue: #81a2be;
$l-magenta: #b294bb;
$l-cyan: #8abeb7;
- $l-white: $ci-text-color;
+ $l-white: $gray-darkest;
+
+ /*
+ * xterm colors
+ */
+ $xterm-fg-0: $black;
+ $xterm-fg-1: #800000;
+ $xterm-fg-2: #008000;
+ $xterm-fg-3: #808000;
+ $xterm-fg-4: #000080;
+ $xterm-fg-5: #800080;
+ $xterm-fg-6: #008080;
+ $xterm-fg-7: #c0c0c0;
+ $xterm-fg-8: #808080;
+ $xterm-fg-9: #f00;
+ $xterm-fg-10: #0f0;
+ $xterm-fg-11: #ff0;
+ $xterm-fg-12: #00f;
+ $xterm-fg-13: #f0f;
+ $xterm-fg-14: #0ff;
+ $xterm-fg-15: $white-light;
+ $xterm-fg-16: $black;
+ $xterm-fg-17: #00005f;
+ $xterm-fg-18: #000087;
+ $xterm-fg-19: #0000af;
+ $xterm-fg-20: #0000d7;
+ $xterm-fg-21: #00f;
+ $xterm-fg-22: #005f00;
+ $xterm-fg-23: #005f5f;
+ $xterm-fg-24: #005f87;
+ $xterm-fg-25: #005faf;
+ $xterm-fg-26: #005fd7;
+ $xterm-fg-27: #005fff;
+ $xterm-fg-28: #008700;
+ $xterm-fg-29: #00875f;
+ $xterm-fg-30: #008787;
+ $xterm-fg-31: #0087af;
+ $xterm-fg-32: #0087d7;
+ $xterm-fg-33: #0087ff;
+ $xterm-fg-34: #00af00;
+ $xterm-fg-35: #00af5f;
+ $xterm-fg-36: #00af87;
+ $xterm-fg-37: #00afaf;
+ $xterm-fg-38: #00afd7;
+ $xterm-fg-39: #00afff;
+ $xterm-fg-40: #00d700;
+ $xterm-fg-41: #00d75f;
+ $xterm-fg-42: #00d787;
+ $xterm-fg-43: #00d7af;
+ $xterm-fg-44: #00d7d7;
+ $xterm-fg-45: #00d7ff;
+ $xterm-fg-46: #0f0;
+ $xterm-fg-47: #00ff5f;
+ $xterm-fg-48: #00ff87;
+ $xterm-fg-49: #00ffaf;
+ $xterm-fg-50: #00ffd7;
+ $xterm-fg-51: #0ff;
+ $xterm-fg-52: #5f0000;
+ $xterm-fg-53: #5f005f;
+ $xterm-fg-54: #5f0087;
+ $xterm-fg-55: #5f00af;
+ $xterm-fg-56: #5f00d7;
+ $xterm-fg-57: #5f00ff;
+ $xterm-fg-58: #5f5f00;
+ $xterm-fg-59: #5f5f5f;
+ $xterm-fg-60: #5f5f87;
+ $xterm-fg-61: #5f5faf;
+ $xterm-fg-62: #5f5fd7;
+ $xterm-fg-63: #5f5fff;
+ $xterm-fg-64: #5f8700;
+ $xterm-fg-65: #5f875f;
+ $xterm-fg-66: #5f8787;
+ $xterm-fg-67: #5f87af;
+ $xterm-fg-68: #5f87d7;
+ $xterm-fg-69: #5f87ff;
+ $xterm-fg-70: #5faf00;
+ $xterm-fg-71: #5faf5f;
+ $xterm-fg-72: #5faf87;
+ $xterm-fg-73: #5fafaf;
+ $xterm-fg-74: #5fafd7;
+ $xterm-fg-75: #5fafff;
+ $xterm-fg-76: #5fd700;
+ $xterm-fg-77: #5fd75f;
+ $xterm-fg-78: #5fd787;
+ $xterm-fg-79: #5fd7af;
+ $xterm-fg-80: #5fd7d7;
+ $xterm-fg-81: #5fd7ff;
+ $xterm-fg-82: #5fff00;
+ $xterm-fg-83: #5fff5f;
+ $xterm-fg-84: #5fff87;
+ $xterm-fg-85: #5fffaf;
+ $xterm-fg-86: #5fffd7;
+ $xterm-fg-87: #5fffff;
+ $xterm-fg-88: #870000;
+ $xterm-fg-89: #87005f;
+ $xterm-fg-90: #870087;
+ $xterm-fg-91: #8700af;
+ $xterm-fg-92: #8700d7;
+ $xterm-fg-93: #8700ff;
+ $xterm-fg-94: #875f00;
+ $xterm-fg-95: #875f5f;
+ $xterm-fg-96: #875f87;
+ $xterm-fg-97: #875faf;
+ $xterm-fg-98: #875fd7;
+ $xterm-fg-99: #875fff;
+ $xterm-fg-100: #878700;
+ $xterm-fg-101: #87875f;
+ $xterm-fg-102: #878787;
+ $xterm-fg-103: #8787af;
+ $xterm-fg-104: #8787d7;
+ $xterm-fg-105: #8787ff;
+ $xterm-fg-106: #87af00;
+ $xterm-fg-107: #87af5f;
+ $xterm-fg-108: #87af87;
+ $xterm-fg-109: #87afaf;
+ $xterm-fg-110: #87afd7;
+ $xterm-fg-111: #87afff;
+ $xterm-fg-112: #87d700;
+ $xterm-fg-113: #87d75f;
+ $xterm-fg-114: #87d787;
+ $xterm-fg-115: #87d7af;
+ $xterm-fg-116: #87d7d7;
+ $xterm-fg-117: #87d7ff;
+ $xterm-fg-118: #87ff00;
+ $xterm-fg-119: #87ff5f;
+ $xterm-fg-120: #87ff87;
+ $xterm-fg-121: #87ffaf;
+ $xterm-fg-122: #87ffd7;
+ $xterm-fg-123: #87ffff;
+ $xterm-fg-124: #af0000;
+ $xterm-fg-125: #af005f;
+ $xterm-fg-126: #af0087;
+ $xterm-fg-127: #af00af;
+ $xterm-fg-128: #af00d7;
+ $xterm-fg-129: #af00ff;
+ $xterm-fg-130: #af5f00;
+ $xterm-fg-131: #af5f5f;
+ $xterm-fg-132: #af5f87;
+ $xterm-fg-133: #af5faf;
+ $xterm-fg-134: #af5fd7;
+ $xterm-fg-135: #af5fff;
+ $xterm-fg-136: #af8700;
+ $xterm-fg-137: #af875f;
+ $xterm-fg-138: #af8787;
+ $xterm-fg-139: #af87af;
+ $xterm-fg-140: #af87d7;
+ $xterm-fg-141: #af87ff;
+ $xterm-fg-142: #afaf00;
+ $xterm-fg-143: #afaf5f;
+ $xterm-fg-144: #afaf87;
+ $xterm-fg-145: #afafaf;
+ $xterm-fg-146: #afafd7;
+ $xterm-fg-147: #afafff;
+ $xterm-fg-148: #afd700;
+ $xterm-fg-149: #afd75f;
+ $xterm-fg-150: #afd787;
+ $xterm-fg-151: #afd7af;
+ $xterm-fg-152: #afd7d7;
+ $xterm-fg-153: #afd7ff;
+ $xterm-fg-154: #afff00;
+ $xterm-fg-155: #afff5f;
+ $xterm-fg-156: #afff87;
+ $xterm-fg-157: #afffaf;
+ $xterm-fg-158: #afffd7;
+ $xterm-fg-159: #afffff;
+ $xterm-fg-160: #d70000;
+ $xterm-fg-161: #d7005f;
+ $xterm-fg-162: #d70087;
+ $xterm-fg-163: #d700af;
+ $xterm-fg-164: #d700d7;
+ $xterm-fg-165: #d700ff;
+ $xterm-fg-166: #d75f00;
+ $xterm-fg-167: #d75f5f;
+ $xterm-fg-168: #d75f87;
+ $xterm-fg-169: #d75faf;
+ $xterm-fg-170: #d75fd7;
+ $xterm-fg-171: #d75fff;
+ $xterm-fg-172: #d78700;
+ $xterm-fg-173: #d7875f;
+ $xterm-fg-174: #d78787;
+ $xterm-fg-175: #d787af;
+ $xterm-fg-176: #d787d7;
+ $xterm-fg-177: #d787ff;
+ $xterm-fg-178: #d7af00;
+ $xterm-fg-179: #d7af5f;
+ $xterm-fg-180: #d7af87;
+ $xterm-fg-181: #d7afaf;
+ $xterm-fg-182: #d7afd7;
+ $xterm-fg-183: #d7afff;
+ $xterm-fg-184: #d7d700;
+ $xterm-fg-185: #d7d75f;
+ $xterm-fg-186: #d7d787;
+ $xterm-fg-187: #d7d7af;
+ $xterm-fg-188: #d7d7d7;
+ $xterm-fg-189: #d7d7ff;
+ $xterm-fg-190: #d7ff00;
+ $xterm-fg-191: #d7ff5f;
+ $xterm-fg-192: #d7ff87;
+ $xterm-fg-193: #d7ffaf;
+ $xterm-fg-194: #d7ffd7;
+ $xterm-fg-195: #d7ffff;
+ $xterm-fg-196: #f00;
+ $xterm-fg-197: #ff005f;
+ $xterm-fg-198: #ff0087;
+ $xterm-fg-199: #ff00af;
+ $xterm-fg-200: #ff00d7;
+ $xterm-fg-201: #f0f;
+ $xterm-fg-202: #ff5f00;
+ $xterm-fg-203: #ff5f5f;
+ $xterm-fg-204: #ff5f87;
+ $xterm-fg-205: #ff5faf;
+ $xterm-fg-206: #ff5fd7;
+ $xterm-fg-207: #ff5fff;
+ $xterm-fg-208: #ff8700;
+ $xterm-fg-209: #ff875f;
+ $xterm-fg-210: #ff8787;
+ $xterm-fg-211: #ff87af;
+ $xterm-fg-212: #ff87d7;
+ $xterm-fg-213: #ff87ff;
+ $xterm-fg-214: #ffaf00;
+ $xterm-fg-215: #ffaf5f;
+ $xterm-fg-216: #ffaf87;
+ $xterm-fg-217: #ffafaf;
+ $xterm-fg-218: #ffafd7;
+ $xterm-fg-219: #ffafff;
+ $xterm-fg-220: #ffd700;
+ $xterm-fg-221: #ffd75f;
+ $xterm-fg-222: #ffd787;
+ $xterm-fg-223: #ffd7af;
+ $xterm-fg-224: #ffd7d7;
+ $xterm-fg-225: #ffd7ff;
+ $xterm-fg-226: #ff0;
+ $xterm-fg-227: #ffff5f;
+ $xterm-fg-228: #ffff87;
+ $xterm-fg-229: #ffffaf;
+ $xterm-fg-230: #ffffd7;
+ $xterm-fg-231: $white-light;
+ $xterm-fg-232: #080808;
+ $xterm-fg-233: #121212;
+ $xterm-fg-234: #1c1c1c;
+ $xterm-fg-235: #262626;
+ $xterm-fg-236: #303030;
+ $xterm-fg-237: #3a3a3a;
+ $xterm-fg-238: #444;
+ $xterm-fg-239: #4e4e4e;
+ $xterm-fg-240: #585858;
+ $xterm-fg-241: #626262;
+ $xterm-fg-242: #6c6c6c;
+ $xterm-fg-243: #767676;
+ $xterm-fg-244: #808080;
+ $xterm-fg-245: #8a8a8a;
+ $xterm-fg-246: #949494;
+ $xterm-fg-247: #9e9e9e;
+ $xterm-fg-248: #a8a8a8;
+ $xterm-fg-249: #b2b2b2;
+ $xterm-fg-250: #bcbcbc;
+ $xterm-fg-251: #c6c6c6;
+ $xterm-fg-252: #d0d0d0;
+ $xterm-fg-253: #dadada;
+ $xterm-fg-254: #e4e4e4;
+ $xterm-fg-255: #eee;
.term-bold {
font-weight: bold;
@@ -169,1026 +429,1026 @@
}
.xterm-fg-0 {
- color: #000;
+ color: $xterm-fg-0;
}
.xterm-fg-1 {
- color: #800000;
+ color: $xterm-fg-1;
}
.xterm-fg-2 {
- color: #008000;
+ color: $xterm-fg-2;
}
.xterm-fg-3 {
- color: #808000;
+ color: $xterm-fg-3;
}
.xterm-fg-4 {
- color: #000080;
+ color: $xterm-fg-4;
}
.xterm-fg-5 {
- color: #800080;
+ color: $xterm-fg-5;
}
.xterm-fg-6 {
- color: #008080;
+ color: $xterm-fg-6;
}
.xterm-fg-7 {
- color: #c0c0c0;
+ color: $xterm-fg-7;
}
.xterm-fg-8 {
- color: #808080;
+ color: $xterm-fg-8;
}
.xterm-fg-9 {
- color: #f00;
+ color: $xterm-fg-9;
}
.xterm-fg-10 {
- color: #0f0;
+ color: $xterm-fg-10;
}
.xterm-fg-11 {
- color: #ff0;
+ color: $xterm-fg-11;
}
.xterm-fg-12 {
- color: #00f;
+ color: $xterm-fg-12;
}
.xterm-fg-13 {
- color: #f0f;
+ color: $xterm-fg-13;
}
.xterm-fg-14 {
- color: #0ff;
+ color: $xterm-fg-14;
}
.xterm-fg-15 {
- color: #fff;
+ color: $white-light;
}
.xterm-fg-16 {
- color: #000;
+ color: $black;
}
.xterm-fg-17 {
- color: #00005f;
+ color: $xterm-fg-17;
}
.xterm-fg-18 {
- color: #000087;
+ color: $xterm-fg-18;
}
.xterm-fg-19 {
- color: #0000af;
+ color: $xterm-fg-19;
}
.xterm-fg-20 {
- color: #0000d7;
+ color: $xterm-fg-20;
}
.xterm-fg-21 {
- color: #00f;
+ color: $xterm-fg-21;
}
.xterm-fg-22 {
- color: #005f00;
+ color: $xterm-fg-22;
}
.xterm-fg-23 {
- color: #005f5f;
+ color: $xterm-fg-23;
}
.xterm-fg-24 {
- color: #005f87;
+ color: $xterm-fg-24;
}
.xterm-fg-25 {
- color: #005faf;
+ color: $xterm-fg-25;
}
.xterm-fg-26 {
- color: #005fd7;
+ color: $xterm-fg-26;
}
.xterm-fg-27 {
- color: #005fff;
+ color: $xterm-fg-27;
}
.xterm-fg-28 {
- color: #008700;
+ color: $xterm-fg-28;
}
.xterm-fg-29 {
- color: #00875f;
+ color: $xterm-fg-29;
}
.xterm-fg-30 {
- color: #008787;
+ color: $xterm-fg-30;
}
.xterm-fg-31 {
- color: #0087af;
+ color: $xterm-fg-31;
}
.xterm-fg-32 {
- color: #0087d7;
+ color: $xterm-fg-32;
}
.xterm-fg-33 {
- color: #0087ff;
+ color: $xterm-fg-33;
}
.xterm-fg-34 {
- color: #00af00;
+ color: $xterm-fg-34;
}
.xterm-fg-35 {
- color: #00af5f;
+ color: $xterm-fg-35;
}
.xterm-fg-36 {
- color: #00af87;
+ color: $xterm-fg-36;
}
.xterm-fg-37 {
- color: #00afaf;
+ color: $xterm-fg-37;
}
.xterm-fg-38 {
- color: #00afd7;
+ color: $xterm-fg-38;
}
.xterm-fg-39 {
- color: #00afff;
+ color: $xterm-fg-39;
}
.xterm-fg-40 {
- color: #00d700;
+ color: $xterm-fg-40;
}
.xterm-fg-41 {
- color: #00d75f;
+ color: $xterm-fg-41;
}
.xterm-fg-42 {
- color: #00d787;
+ color: $xterm-fg-42;
}
.xterm-fg-43 {
- color: #00d7af;
+ color: $xterm-fg-43;
}
.xterm-fg-44 {
- color: #00d7d7;
+ color: $xterm-fg-44;
}
.xterm-fg-45 {
- color: #00d7ff;
+ color: $xterm-fg-45;
}
.xterm-fg-46 {
- color: #0f0;
+ color: $xterm-fg-46;
}
.xterm-fg-47 {
- color: #00ff5f;
+ color: $xterm-fg-47;
}
.xterm-fg-48 {
- color: #00ff87;
+ color: $xterm-fg-48;
}
.xterm-fg-49 {
- color: #00ffaf;
+ color: $xterm-fg-49;
}
.xterm-fg-50 {
- color: #00ffd7;
+ color: $xterm-fg-50;
}
.xterm-fg-51 {
- color: #0ff;
+ color: $xterm-fg-51;
}
.xterm-fg-52 {
- color: #5f0000;
+ color: $xterm-fg-52;
}
.xterm-fg-53 {
- color: #5f005f;
+ color: $xterm-fg-53;
}
.xterm-fg-54 {
- color: #5f0087;
+ color: $xterm-fg-54;
}
.xterm-fg-55 {
- color: #5f00af;
+ color: $xterm-fg-55;
}
.xterm-fg-56 {
- color: #5f00d7;
+ color: $xterm-fg-56;
}
.xterm-fg-57 {
- color: #5f00ff;
+ color: $xterm-fg-57;
}
.xterm-fg-58 {
- color: #5f5f00;
+ color: $xterm-fg-58;
}
.xterm-fg-59 {
- color: #5f5f5f;
+ color: $xterm-fg-59;
}
.xterm-fg-60 {
- color: #5f5f87;
+ color: $xterm-fg-60;
}
.xterm-fg-61 {
- color: #5f5faf;
+ color: $xterm-fg-61;
}
.xterm-fg-62 {
- color: #5f5fd7;
+ color: $xterm-fg-62;
}
.xterm-fg-63 {
- color: #5f5fff;
+ color: $xterm-fg-63;
}
.xterm-fg-64 {
- color: #5f8700;
+ color: $xterm-fg-64;
}
.xterm-fg-65 {
- color: #5f875f;
+ color: $xterm-fg-65;
}
.xterm-fg-66 {
- color: #5f8787;
+ color: $xterm-fg-66;
}
.xterm-fg-67 {
- color: #5f87af;
+ color: $xterm-fg-67;
}
.xterm-fg-68 {
- color: #5f87d7;
+ color: $xterm-fg-68;
}
.xterm-fg-69 {
- color: #5f87ff;
+ color: $xterm-fg-69;
}
.xterm-fg-70 {
- color: #5faf00;
+ color: $xterm-fg-70;
}
.xterm-fg-71 {
- color: #5faf5f;
+ color: $xterm-fg-71;
}
.xterm-fg-72 {
- color: #5faf87;
+ color: $xterm-fg-72;
}
.xterm-fg-73 {
- color: #5fafaf;
+ color: $xterm-fg-73;
}
.xterm-fg-74 {
- color: #5fafd7;
+ color: $xterm-fg-74;
}
.xterm-fg-75 {
- color: #5fafff;
+ color: $xterm-fg-75;
}
.xterm-fg-76 {
- color: #5fd700;
+ color: $xterm-fg-76;
}
.xterm-fg-77 {
- color: #5fd75f;
+ color: $xterm-fg-77;
}
.xterm-fg-78 {
- color: #5fd787;
+ color: $xterm-fg-78;
}
.xterm-fg-79 {
- color: #5fd7af;
+ color: $xterm-fg-79;
}
.xterm-fg-80 {
- color: #5fd7d7;
+ color: $xterm-fg-80;
}
.xterm-fg-81 {
- color: #5fd7ff;
+ color: $xterm-fg-81;
}
.xterm-fg-82 {
- color: #5fff00;
+ color: $xterm-fg-82;
}
.xterm-fg-83 {
- color: #5fff5f;
+ color: $xterm-fg-83;
}
.xterm-fg-84 {
- color: #5fff87;
+ color: $xterm-fg-84;
}
.xterm-fg-85 {
- color: #5fffaf;
+ color: $xterm-fg-85;
}
.xterm-fg-86 {
- color: #5fffd7;
+ color: $xterm-fg-86;
}
.xterm-fg-87 {
- color: #5fffff;
+ color: $xterm-fg-87;
}
.xterm-fg-88 {
- color: #870000;
+ color: $xterm-fg-88;
}
.xterm-fg-89 {
- color: #87005f;
+ color: $xterm-fg-89;
}
.xterm-fg-90 {
- color: #870087;
+ color: $xterm-fg-90;
}
.xterm-fg-91 {
- color: #8700af;
+ color: $xterm-fg-91;
}
.xterm-fg-92 {
- color: #8700d7;
+ color: $xterm-fg-92;
}
.xterm-fg-93 {
- color: #8700ff;
+ color: $xterm-fg-93;
}
.xterm-fg-94 {
- color: #875f00;
+ color: $xterm-fg-94;
}
.xterm-fg-95 {
- color: #875f5f;
+ color: $xterm-fg-95;
}
.xterm-fg-96 {
- color: #875f87;
+ color: $xterm-fg-96;
}
.xterm-fg-97 {
- color: #875faf;
+ color: $xterm-fg-97;
}
.xterm-fg-98 {
- color: #875fd7;
+ color: $xterm-fg-98;
}
.xterm-fg-99 {
- color: #875fff;
+ color: $xterm-fg-99;
}
.xterm-fg-100 {
- color: #878700;
+ color: $xterm-fg-100;
}
.xterm-fg-101 {
- color: #87875f;
+ color: $xterm-fg-101;
}
.xterm-fg-102 {
- color: #878787;
+ color: $xterm-fg-102;
}
.xterm-fg-103 {
- color: #8787af;
+ color: $xterm-fg-103;
}
.xterm-fg-104 {
- color: #8787d7;
+ color: $xterm-fg-104;
}
.xterm-fg-105 {
- color: #8787ff;
+ color: $xterm-fg-105;
}
.xterm-fg-106 {
- color: #87af00;
+ color: $xterm-fg-106;
}
.xterm-fg-107 {
- color: #87af5f;
+ color: $xterm-fg-107;
}
.xterm-fg-108 {
- color: #87af87;
+ color: $xterm-fg-108;
}
.xterm-fg-109 {
- color: #87afaf;
+ color: $xterm-fg-109;
}
.xterm-fg-110 {
- color: #87afd7;
+ color: $xterm-fg-110;
}
.xterm-fg-111 {
- color: #87afff;
+ color: $xterm-fg-111;
}
.xterm-fg-112 {
- color: #87d700;
+ color: $xterm-fg-112;
}
.xterm-fg-113 {
- color: #87d75f;
+ color: $xterm-fg-113;
}
.xterm-fg-114 {
- color: #87d787;
+ color: $xterm-fg-114;
}
.xterm-fg-115 {
- color: #87d7af;
+ color: $xterm-fg-115;
}
.xterm-fg-116 {
- color: #87d7d7;
+ color: $xterm-fg-116;
}
.xterm-fg-117 {
- color: #87d7ff;
+ color: $xterm-fg-117;
}
.xterm-fg-118 {
- color: #87ff00;
+ color: $xterm-fg-118;
}
.xterm-fg-119 {
- color: #87ff5f;
+ color: $xterm-fg-119;
}
.xterm-fg-120 {
- color: #87ff87;
+ color: $xterm-fg-120;
}
.xterm-fg-121 {
- color: #87ffaf;
+ color: $xterm-fg-121;
}
.xterm-fg-122 {
- color: #87ffd7;
+ color: $xterm-fg-122;
}
.xterm-fg-123 {
- color: #87ffff;
+ color: $xterm-fg-123;
}
.xterm-fg-124 {
- color: #af0000;
+ color: $xterm-fg-124;
}
.xterm-fg-125 {
- color: #af005f;
+ color: $xterm-fg-125;
}
.xterm-fg-126 {
- color: #af0087;
+ color: $xterm-fg-126;
}
.xterm-fg-127 {
- color: #af00af;
+ color: $xterm-fg-127;
}
.xterm-fg-128 {
- color: #af00d7;
+ color: $xterm-fg-128;
}
.xterm-fg-129 {
- color: #af00ff;
+ color: $xterm-fg-129;
}
.xterm-fg-130 {
- color: #af5f00;
+ color: $xterm-fg-130;
}
.xterm-fg-131 {
- color: #af5f5f;
+ color: $xterm-fg-131;
}
.xterm-fg-132 {
- color: #af5f87;
+ color: $xterm-fg-132;
}
.xterm-fg-133 {
- color: #af5faf;
+ color: $xterm-fg-133;
}
.xterm-fg-134 {
- color: #af5fd7;
+ color: $xterm-fg-134;
}
.xterm-fg-135 {
- color: #af5fff;
+ color: $xterm-fg-135;
}
.xterm-fg-136 {
- color: #af8700;
+ color: $xterm-fg-136;
}
.xterm-fg-137 {
- color: #af875f;
+ color: $xterm-fg-137;
}
.xterm-fg-138 {
- color: #af8787;
+ color: $xterm-fg-138;
}
.xterm-fg-139 {
- color: #af87af;
+ color: $xterm-fg-139;
}
.xterm-fg-140 {
- color: #af87d7;
+ color: $xterm-fg-140;
}
.xterm-fg-141 {
- color: #af87ff;
+ color: $xterm-fg-141;
}
.xterm-fg-142 {
- color: #afaf00;
+ color: $xterm-fg-142;
}
.xterm-fg-143 {
- color: #afaf5f;
+ color: $xterm-fg-143;
}
.xterm-fg-144 {
- color: #afaf87;
+ color: $xterm-fg-144;
}
.xterm-fg-145 {
- color: #afafaf;
+ color: $xterm-fg-145;
}
.xterm-fg-146 {
- color: #afafd7;
+ color: $xterm-fg-146;
}
.xterm-fg-147 {
- color: #afafff;
+ color: $xterm-fg-147;
}
.xterm-fg-148 {
- color: #afd700;
+ color: $xterm-fg-148;
}
.xterm-fg-149 {
- color: #afd75f;
+ color: $xterm-fg-149;
}
.xterm-fg-150 {
- color: #afd787;
+ color: $xterm-fg-150;
}
.xterm-fg-151 {
- color: #afd7af;
+ color: $xterm-fg-151;
}
.xterm-fg-152 {
- color: #afd7d7;
+ color: $xterm-fg-152;
}
.xterm-fg-153 {
- color: #afd7ff;
+ color: $xterm-fg-153;
}
.xterm-fg-154 {
- color: #afff00;
+ color: $xterm-fg-154;
}
.xterm-fg-155 {
- color: #afff5f;
+ color: $xterm-fg-155;
}
.xterm-fg-156 {
- color: #afff87;
+ color: $xterm-fg-156;
}
.xterm-fg-157 {
- color: #afffaf;
+ color: $xterm-fg-157;
}
.xterm-fg-158 {
- color: #afffd7;
+ color: $xterm-fg-158;
}
.xterm-fg-159 {
- color: #afffff;
+ color: $xterm-fg-159;
}
.xterm-fg-160 {
- color: #d70000;
+ color: $xterm-fg-160;
}
.xterm-fg-161 {
- color: #d7005f;
+ color: $xterm-fg-161;
}
.xterm-fg-162 {
- color: #d70087;
+ color: $xterm-fg-162;
}
.xterm-fg-163 {
- color: #d700af;
+ color: $xterm-fg-163;
}
.xterm-fg-164 {
- color: #d700d7;
+ color: $xterm-fg-164;
}
.xterm-fg-165 {
- color: #d700ff;
+ color: $xterm-fg-165;
}
.xterm-fg-166 {
- color: #d75f00;
+ color: $xterm-fg-166;
}
.xterm-fg-167 {
- color: #d75f5f;
+ color: $xterm-fg-167;
}
.xterm-fg-168 {
- color: #d75f87;
+ color: $xterm-fg-168;
}
.xterm-fg-169 {
- color: #d75faf;
+ color: $xterm-fg-169;
}
.xterm-fg-170 {
- color: #d75fd7;
+ color: $xterm-fg-170;
}
.xterm-fg-171 {
- color: #d75fff;
+ color: $xterm-fg-171;
}
.xterm-fg-172 {
- color: #d78700;
+ color: $xterm-fg-172;
}
.xterm-fg-173 {
- color: #d7875f;
+ color: $xterm-fg-173;
}
.xterm-fg-174 {
- color: #d78787;
+ color: $xterm-fg-174;
}
.xterm-fg-175 {
- color: #d787af;
+ color: $xterm-fg-175;
}
.xterm-fg-176 {
- color: #d787d7;
+ color: $xterm-fg-176;
}
.xterm-fg-177 {
- color: #d787ff;
+ color: $xterm-fg-177;
}
.xterm-fg-178 {
- color: #d7af00;
+ color: $xterm-fg-178;
}
.xterm-fg-179 {
- color: #d7af5f;
+ color: $xterm-fg-179;
}
.xterm-fg-180 {
- color: #d7af87;
+ color: $xterm-fg-180;
}
.xterm-fg-181 {
- color: #d7afaf;
+ color: $xterm-fg-181;
}
.xterm-fg-182 {
- color: #d7afd7;
+ color: $xterm-fg-182;
}
.xterm-fg-183 {
- color: #d7afff;
+ color: $xterm-fg-183;
}
.xterm-fg-184 {
- color: #d7d700;
+ color: $xterm-fg-184;
}
.xterm-fg-185 {
- color: #d7d75f;
+ color: $xterm-fg-185;
}
.xterm-fg-186 {
- color: #d7d787;
+ color: $xterm-fg-186;
}
.xterm-fg-187 {
- color: #d7d7af;
+ color: $xterm-fg-187;
}
.xterm-fg-188 {
- color: #d7d7d7;
+ color: $xterm-fg-188;
}
.xterm-fg-189 {
- color: #d7d7ff;
+ color: $xterm-fg-189;
}
.xterm-fg-190 {
- color: #d7ff00;
+ color: $xterm-fg-190;
}
.xterm-fg-191 {
- color: #d7ff5f;
+ color: $xterm-fg-191;
}
.xterm-fg-192 {
- color: #d7ff87;
+ color: $xterm-fg-192;
}
.xterm-fg-193 {
- color: #d7ffaf;
+ color: $xterm-fg-193;
}
.xterm-fg-194 {
- color: #d7ffd7;
+ color: $xterm-fg-194;
}
.xterm-fg-195 {
- color: #d7ffff;
+ color: $xterm-fg-195;
}
.xterm-fg-196 {
- color: #f00;
+ color: $xterm-fg-196;
}
.xterm-fg-197 {
- color: #ff005f;
+ color: $xterm-fg-197;
}
.xterm-fg-198 {
- color: #ff0087;
+ color: $xterm-fg-198;
}
.xterm-fg-199 {
- color: #ff00af;
+ color: $xterm-fg-199;
}
.xterm-fg-200 {
- color: #ff00d7;
+ color: $xterm-fg-200;
}
.xterm-fg-201 {
- color: #f0f;
+ color: $xterm-fg-201;
}
.xterm-fg-202 {
- color: #ff5f00;
+ color: $xterm-fg-202;
}
.xterm-fg-203 {
- color: #ff5f5f;
+ color: $xterm-fg-203;
}
.xterm-fg-204 {
- color: #ff5f87;
+ color: $xterm-fg-204;
}
.xterm-fg-205 {
- color: #ff5faf;
+ color: $xterm-fg-205;
}
.xterm-fg-206 {
- color: #ff5fd7;
+ color: $xterm-fg-206;
}
.xterm-fg-207 {
- color: #ff5fff;
+ color: $xterm-fg-207;
}
.xterm-fg-208 {
- color: #ff8700;
+ color: $xterm-fg-208;
}
.xterm-fg-209 {
- color: #ff875f;
+ color: $xterm-fg-209;
}
.xterm-fg-210 {
- color: #ff8787;
+ color: $xterm-fg-210;
}
.xterm-fg-211 {
- color: #ff87af;
+ color: $xterm-fg-211;
}
.xterm-fg-212 {
- color: #ff87d7;
+ color: $xterm-fg-212;
}
.xterm-fg-213 {
- color: #ff87ff;
+ color: $xterm-fg-213;
}
.xterm-fg-214 {
- color: #ffaf00;
+ color: $xterm-fg-214;
}
.xterm-fg-215 {
- color: #ffaf5f;
+ color: $xterm-fg-215;
}
.xterm-fg-216 {
- color: #ffaf87;
+ color: $xterm-fg-216;
}
.xterm-fg-217 {
- color: #ffafaf;
+ color: $xterm-fg-217;
}
.xterm-fg-218 {
- color: #ffafd7;
+ color: $xterm-fg-218;
}
.xterm-fg-219 {
- color: #ffafff;
+ color: $xterm-fg-219;
}
.xterm-fg-220 {
- color: #ffd700;
+ color: $xterm-fg-220;
}
.xterm-fg-221 {
- color: #ffd75f;
+ color: $xterm-fg-221;
}
.xterm-fg-222 {
- color: #ffd787;
+ color: $xterm-fg-222;
}
.xterm-fg-223 {
- color: #ffd7af;
+ color: $xterm-fg-223;
}
.xterm-fg-224 {
- color: #ffd7d7;
+ color: $xterm-fg-224;
}
.xterm-fg-225 {
- color: #ffd7ff;
+ color: $xterm-fg-225;
}
.xterm-fg-226 {
- color: #ff0;
+ color: $xterm-fg-226;
}
.xterm-fg-227 {
- color: #ffff5f;
+ color: $xterm-fg-227;
}
.xterm-fg-228 {
- color: #ffff87;
+ color: $xterm-fg-228;
}
.xterm-fg-229 {
- color: #ffffaf;
+ color: $xterm-fg-229;
}
.xterm-fg-230 {
- color: #ffffd7;
+ color: $xterm-fg-230;
}
.xterm-fg-231 {
- color: #fff;
+ color: $xterm-fg-231;
}
.xterm-fg-232 {
- color: #080808;
+ color: $xterm-fg-232;
}
.xterm-fg-233 {
- color: #121212;
+ color: $xterm-fg-233;
}
.xterm-fg-234 {
- color: #1c1c1c;
+ color: $xterm-fg-234;
}
.xterm-fg-235 {
- color: #262626;
+ color: $xterm-fg-235;
}
.xterm-fg-236 {
- color: #303030;
+ color: $xterm-fg-236;
}
.xterm-fg-237 {
- color: #3a3a3a;
+ color: $xterm-fg-237;
}
.xterm-fg-238 {
- color: #444;
+ color: $xterm-fg-238;
}
.xterm-fg-239 {
- color: #4e4e4e;
+ color: $xterm-fg-239;
}
.xterm-fg-240 {
- color: #585858;
+ color: $xterm-fg-240;
}
.xterm-fg-241 {
- color: #626262;
+ color: $xterm-fg-241;
}
.xterm-fg-242 {
- color: #6c6c6c;
+ color: $xterm-fg-242;
}
.xterm-fg-243 {
- color: #767676;
+ color: $xterm-fg-243;
}
.xterm-fg-244 {
- color: #808080;
+ color: $xterm-fg-244;
}
.xterm-fg-245 {
- color: #8a8a8a;
+ color: $xterm-fg-245;
}
.xterm-fg-246 {
- color: #949494;
+ color: $xterm-fg-246;
}
.xterm-fg-247 {
- color: #9e9e9e;
+ color: $xterm-fg-247;
}
.xterm-fg-248 {
- color: #a8a8a8;
+ color: $xterm-fg-248;
}
.xterm-fg-249 {
- color: #b2b2b2;
+ color: $xterm-fg-249;
}
.xterm-fg-250 {
- color: #bcbcbc;
+ color: $xterm-fg-250;
}
.xterm-fg-251 {
- color: #c6c6c6;
+ color: $xterm-fg-251;
}
.xterm-fg-252 {
- color: #d0d0d0;
+ color: $xterm-fg-252;
}
.xterm-fg-253 {
- color: #dadada;
+ color: $xterm-fg-253;
}
.xterm-fg-254 {
- color: #e4e4e4;
+ color: $xterm-fg-254;
}
.xterm-fg-255 {
- color: #eee;
+ color: $xterm-fg-255;
}
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 52e0256943a..1b4987dd738 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -67,65 +67,78 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
- :default_projects_limit,
- :default_branch_protection,
- :signup_enabled,
- :signin_enabled,
- :require_two_factor_authentication,
- :two_factor_grace_period,
- :gravatar_enabled,
- :sign_in_text,
- :after_sign_up_text,
- :help_page_text,
- :home_page_url,
+ application_setting_params_ce
+ )
+ end
+
+ def application_setting_params_ce
+ [
+ :admin_notification_email,
:after_sign_out_path,
- :max_attachment_size,
- :session_expire_delay,
+ :after_sign_up_text,
+ :akismet_api_key,
+ :akismet_enabled,
+ :container_registry_token_expire_delay,
+ :default_branch_protection,
+ :default_group_visibility,
:default_project_visibility,
+ :default_projects_limit,
:default_snippet_visibility,
- :default_group_visibility,
- :domain_whitelist_raw,
:domain_blacklist_enabled,
- :domain_blacklist_raw,
:domain_blacklist_file,
- :version_check_enabled,
- :admin_notification_email,
- :user_oauth_applications,
- :user_default_external,
- :shared_runners_enabled,
- :shared_runners_text,
+ :domain_blacklist_raw,
+ :domain_whitelist_raw,
+ :email_author_in_body,
+ :enabled_git_access_protocol,
+ :gravatar_enabled,
+ :help_page_text,
+ :home_page_url,
+ :housekeeping_bitmaps_enabled,
+ :housekeeping_enabled,
+ :housekeeping_full_repack_period,
+ :housekeeping_gc_period,
+ :housekeeping_incremental_repack_period,
+ :html_emails_enabled,
+ :koding_enabled,
+ :koding_url,
+ :plantuml_enabled,
+ :plantuml_url,
:max_artifacts_size,
+ :max_attachment_size,
:metrics_enabled,
:metrics_host,
- :metrics_port,
- :metrics_pool_size,
- :metrics_timeout,
:metrics_method_call_threshold,
+ :metrics_packet_size,
+ :metrics_pool_size,
+ :metrics_port,
:metrics_sample_interval,
+ :metrics_timeout,
:recaptcha_enabled,
- :recaptcha_site_key,
:recaptcha_private_key,
- :sentry_enabled,
- :sentry_dsn,
- :akismet_enabled,
- :akismet_api_key,
- :koding_enabled,
- :koding_url,
- :email_author_in_body,
+ :recaptcha_site_key,
:repository_checks_enabled,
- :metrics_packet_size,
+ :require_two_factor_authentication,
+ :session_expire_delay,
+ :sign_in_text,
+ :signin_enabled,
+ :signup_enabled,
+ :sentry_dsn,
+ :sentry_enabled,
:send_user_confirmation_email,
- :container_registry_token_expire_delay,
- :enabled_git_access_protocol,
- :housekeeping_enabled,
- :housekeeping_bitmaps_enabled,
- :housekeeping_incremental_repack_period,
- :housekeeping_full_repack_period,
- :housekeeping_gc_period,
+ :shared_runners_enabled,
+ :shared_runners_text,
+ :sidekiq_throttling_enabled,
+ :sidekiq_throttling_factor,
+ :two_factor_grace_period,
+ :user_default_external,
+ :user_oauth_applications,
+ :version_check_enabled,
+
+ disabled_oauth_sign_in_sources: [],
+ import_sources: [],
repository_storages: [],
restricted_visibility_levels: [],
- import_sources: [],
- disabled_oauth_sign_in_sources: []
- )
+ sidekiq_throttling_queues: []
+ ]
end
end
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 471d24934a0..62f62e99a97 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -1,5 +1,8 @@
class Admin::ApplicationsController < Admin::ApplicationController
+ include OauthApplications
+
before_action :set_application, only: [:show, :edit, :update, :destroy]
+ before_action :load_scopes, only: [:new, :edit]
def index
@applications = Doorkeeper::Application.where("owner_id IS NULL")
@@ -47,6 +50,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through.
def application_params
- params[:doorkeeper_application].permit(:name, :redirect_uri)
+ params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes)
end
end
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
index 285e8495342..4f6a7e9e2cb 100644
--- a/app/controllers/admin/deploy_keys_controller.rb
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -10,7 +10,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def create
- @deploy_key = deploy_keys.new(deploy_key_params)
+ @deploy_key = deploy_keys.new(deploy_key_params.merge(user: current_user))
if @deploy_key.save
redirect_to admin_deploy_keys_path
@@ -39,6 +39,6 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def deploy_key_params
- params.require(:deploy_key).permit(:key, :title)
+ params.require(:deploy_key).permit(:key, :title, :can_push)
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index aa7570cd896..b7722a1d15d 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -1,17 +1,18 @@
class Admin::GroupsController < Admin::ApplicationController
- before_action :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update]
+ before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index
- @groups = Group.all
+ @groups = Group.with_statistics
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page])
end
def show
+ @group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id])
@members = @group.members.order("access_level DESC").page(params[:members_page])
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
- @projects = @group.projects.page(params[:projects_page])
+ @projects = @group.projects.with_statistics.page(params[:projects_page])
end
def new
@@ -56,11 +57,15 @@ class Admin::GroupsController < Admin::ApplicationController
private
def group
- @group ||= Group.find_by(path: params[:id])
+ @group ||= Group.find_by_full_path(params[:id])
end
def group_params
- params.require(:group).permit(
+ params.require(:group).permit(group_params_ce)
+ end
+
+ def group_params_ce
+ [
:avatar,
:description,
:lfs_enabled,
@@ -68,6 +73,6 @@ class Admin::GroupsController < Admin::ApplicationController
:path,
:request_access_enabled,
:visibility_level
- )
+ ]
end
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 1d963bdf7d5..b09ae423096 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -3,7 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer]
def index
- @projects = Project.all
+ @projects = Project.with_statistics
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.with_push if params[:with_push].present?
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index bb912ed10cc..aa0f8d434dc 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -16,9 +16,6 @@ class Admin::UsersController < Admin::ApplicationController
@joined_projects = user.projects.joined(@user)
end
- def groups
- end
-
def keys
@keys = user.keys
end
@@ -164,15 +161,6 @@ class Admin::UsersController < Admin::ApplicationController
@user ||= User.find_by!(username: params[:id])
end
- def user_params
- params.require(:user).permit(
- :email, :remember_me, :bio, :name, :username,
- :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
- :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
- :projects_limit, :can_create_group, :admin, :key_id, :external
- )
- end
-
def redirect_back_or_admin_user(options = {})
redirect_back_or_default(default: default_route, options: options)
end
@@ -180,4 +168,36 @@ class Admin::UsersController < Admin::ApplicationController
def default_route
[:admin, @user]
end
+
+ def user_params
+ params.require(:user).permit(user_params_ce)
+ end
+
+ def user_params_ce
+ [
+ :admin,
+ :avatar,
+ :bio,
+ :can_create_group,
+ :color_scheme_id,
+ :email,
+ :extern_uid,
+ :external,
+ :force_random_password,
+ :hide_no_password,
+ :hide_no_ssh_key,
+ :key_id,
+ :linkedin,
+ :name,
+ :password_expires_at,
+ :projects_limit,
+ :provider,
+ :remember_me,
+ :skype,
+ :theme_id,
+ :twitter,
+ :username,
+ :website_url
+ ]
+ end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 517ad4f03f3..bb47e2a8bf7 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :can?, :current_application_settings
- helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
+ helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -49,6 +49,14 @@ class ApplicationController < ActionController::Base
render_404
end
+ def route_not_found
+ if current_user
+ not_found
+ else
+ redirect_to new_user_session_path
+ end
+ end
+
protected
# This filter handles both private tokens and personal access tokens
@@ -224,7 +232,7 @@ class ApplicationController < ActionController::Base
end
def require_email
- if current_user && current_user.temp_oauth_email?
+ if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil?
redirect_to profile_path, notice: 'Please complete your profile with email address' and return
end
end
@@ -237,6 +245,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('github')
end
+ def gitea_import_enabled?
+ current_application_settings.import_sources.include?('gitea')
+ end
+
def github_import_configured?
Gitlab::OAuth::Provider.enabled?(:github)
end
@@ -254,7 +266,7 @@ class ApplicationController < ActionController::Base
end
def bitbucket_import_configured?
- Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
+ Gitlab::OAuth::Provider.enabled?(:bitbucket)
end
def google_code_import_enabled?
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index daa82336208..6db4e1dc1bc 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -11,14 +11,14 @@ class AutocompleteController < ApplicationController
@users = @users.reorder(:name)
@users = @users.page(params[:page])
- if params[:todo_filter].present?
+ if params[:todo_filter].present? && current_user
@users = @users.todo_authors(current_user.id, params[:todo_state_filter])
end
if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user].present? && current_user
- @users = [*@users, current_user]
+ @users = [current_user, *@users]
end
if params[:author_id].present?
@@ -55,7 +55,13 @@ class AutocompleteController < ApplicationController
def find_users
@users =
if @project
- @project.team.users
+ user_ids = @project.team.users.pluck(:id)
+
+ if params[:author_id].present?
+ user_ids << params[:author_id]
+ end
+
+ User.where(id: user_ids)
elsif params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index dacb5679dd3..6f43ce5226d 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -81,10 +81,8 @@ module CreatesCommit
def merge_request_exists?
return @merge_request if defined?(@merge_request)
- @merge_request = @mr_target_project.merge_requests.opened.find_by(
- source_branch: @mr_source_branch,
- target_branch: @mr_target_branch
- )
+ @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened.
+ find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch, source_project_id: @mr_source_project)
end
def different_project?
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
new file mode 100644
index 00000000000..52e06f4945a
--- /dev/null
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -0,0 +1,11 @@
+module CycleAnalyticsParams
+ extend ActiveSupport::Concern
+
+ def options(params)
+ @options ||= { from: start_date(params), current_user: current_user }
+ end
+
+ def start_date(params)
+ params[:start_date] == '30' ? 30.days.ago : 90.days.ago
+ end
+end
diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb
index aeec3009f15..1efa9fe060f 100644
--- a/app/controllers/concerns/diff_for_path.rb
+++ b/app/controllers/concerns/diff_for_path.rb
@@ -3,7 +3,7 @@ module DiffForPath
def render_diff_for_path(diffs)
diff_file = diffs.diff_files.find do |diff|
- diff.old_path == params[:old_path] && diff.new_path == params[:new_path]
+ diff.file_identifier == params[:file_identifier]
end
return render_404 unless diff_file
diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb
deleted file mode 100644
index 5c503c5b698..00000000000
--- a/app/controllers/concerns/global_milestones.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module GlobalMilestones
- extend ActiveSupport::Concern
-
- def milestones
- epoch = DateTime.parse('1970-01-01')
- @milestones = MilestonesFinder.new.execute(@projects, params)
- @milestones = GlobalMilestone.build_collection(@milestones)
- @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
- end
-
- def milestone
- milestones = Milestone.of_projects(@projects).where(title: params[:title])
-
- if milestones.present?
- @milestone = GlobalMilestone.new(params[:title], milestones)
- else
- render_404
- end
- end
-end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index be86fa106f8..0821974aa93 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -12,7 +12,7 @@ module IssuableActions
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
TodoService.new.public_send(destroy_method, issuable, current_user)
- name = issuable.class.name.titleize.downcase
+ name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index b5e79099e39..6247934f81e 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -10,11 +10,11 @@ module IssuableCollections
private
def issues_collection
- issues_finder.execute
+ issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
end
def merge_requests_collection
- merge_requests_finder.execute
+ merge_requests_finder.execute.preload(:source_project, :target_project, :author, :assignee, :labels, :milestone, :merge_request_diff, target_project: :namespace)
end
def issues_finder
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index b89fb94be6e..b46adcceb60 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -7,7 +7,6 @@ module IssuesAction
@issues = issues_collection
.non_archived
- .preload(:author, :project)
.page(params[:page])
respond_to do |format|
diff --git a/app/helpers/lfs_helper.rb b/app/controllers/concerns/lfs_request.rb
index d3966ba1f10..ed22b1e5470 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -1,5 +1,21 @@
-module LfsHelper
- include Gitlab::Routing.url_helpers
+# This concern assumes:
+# - a `#project` accessor
+# - a `#user` accessor
+# - a `#authentication_result` accessor
+# - a `#can?(object, action, subject)` method
+# - a `#ci?` method
+# - a `#download_request?` method
+# - a `#upload_request?` method
+# - a `#has_authentication_ability?(ability)` method
+module LfsRequest
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :require_lfs_enabled!
+ before_action :lfs_check_access!
+ end
+
+ private
def require_lfs_enabled!
return if Gitlab.config.lfs.enabled
@@ -17,31 +33,15 @@ module LfsHelper
return if download_request? && lfs_download_access?
return if upload_request? && lfs_upload_access?
- if project.public? || (user && user.can?(:read_project, project))
- render_lfs_forbidden
+ if project.public? || can?(user, :read_project, project)
+ lfs_forbidden!
else
render_lfs_not_found
end
end
- def lfs_download_access?
- return false unless project.lfs_enabled?
-
- ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
- end
-
- def user_can_download_code?
- has_authentication_ability?(:download_code) && can?(user, :download_code, project)
- end
-
- def build_can_download_code?
- has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
- end
-
- def lfs_upload_access?
- return false unless project.lfs_enabled?
-
- has_authentication_ability?(:push_code) && can?(user, :push_code, project)
+ def lfs_forbidden!
+ render_lfs_forbidden
end
def render_lfs_forbidden
@@ -66,6 +66,30 @@ module LfsHelper
)
end
+ def lfs_download_access?
+ return false unless project.lfs_enabled?
+
+ ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
+ end
+
+ def lfs_upload_access?
+ return false unless project.lfs_enabled?
+
+ has_authentication_ability?(:push_code) && can?(user, :push_code, project)
+ end
+
+ def lfs_deploy_token?
+ authentication_result.lfs_deploy_token?(project)
+ end
+
+ def user_can_download_code?
+ has_authentication_ability?(:download_code) && can?(user, :download_code, project)
+ end
+
+ def build_can_download_code?
+ has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
+ end
+
def storage_project
@storage_project ||= begin
result = project
@@ -78,4 +102,8 @@ module LfsHelper
result
end
end
+
+ def objects
+ @objects ||= (params[:objects] || []).to_a
+ end
end
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index a1b0eee37f9..fdb05bb3228 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -6,8 +6,12 @@ module MergeRequestsAction
@label = merge_requests_finder.labels.first
@merge_requests = merge_requests_collection
- .non_archived
- .preload(:author, :target_project)
.page(params[:page])
end
+
+ private
+
+ def filter_params
+ super.merge(non_archived: true)
+ end
end
diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb
new file mode 100644
index 00000000000..9849aa93fa6
--- /dev/null
+++ b/app/controllers/concerns/oauth_applications.rb
@@ -0,0 +1,19 @@
+module OauthApplications
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :prepare_scopes, only: [:create, :update]
+ end
+
+ def prepare_scopes
+ scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
+
+ if scopes
+ params[:doorkeeper_application][:scopes] = scopes.join(' ')
+ end
+ end
+
+ def load_scopes
+ @scopes = Doorkeeper.configuration.scopes
+ end
+end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index c33d7eecb9f..d7f5a4e4682 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -1,31 +1,72 @@
module ServiceParams
extend ActiveSupport::Concern
- ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain,
- :room, :recipients, :project_url, :webhook,
- :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
- :build_key, :server, :teamcity_url, :drone_url, :build_type,
- :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
- :colorize_messages, :channels,
- # We're using `issues_events` and `merge_requests_events`
- # in the view so we still need to explicitly state them
- # here. `Service#event_names` would only give
- # `issue_events` and `merge_request_events` (singular!)
- # See app/helpers/services_helper.rb for how we
- # make those event names plural as special case.
- :issues_events, :confidential_issues_events, :merge_requests_events,
- :notify_only_broken_builds, :notify_only_broken_pipelines,
- :add_pusher, :send_from_committer_email, :disable_diffs,
- :external_wiki_url, :notify, :color,
- :server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
- :jira_issue_transition_id, :url, :project_key]
+ ALLOWED_PARAMS_CE = [
+ :active,
+ :add_pusher,
+ :api_key,
+ :api_url,
+ :api_version,
+ :bamboo_url,
+ :build_key,
+ :build_type,
+ :ca_pem,
+ :channel,
+ :channels,
+ :color,
+ :colorize_messages,
+ :confidential_issues_events,
+ :default_irc_uri,
+ :description,
+ :device,
+ :disable_diffs,
+ :drone_url,
+ :enable_ssl_verification,
+ :external_wiki_url,
+ # We're using `issues_events` and `merge_requests_events`
+ # in the view so we still need to explicitly state them
+ # here. `Service#event_names` would only give
+ # `issue_events` and `merge_request_events` (singular!)
+ # See app/helpers/services_helper.rb for how we
+ # make those event names plural as special case.
+ :issues_events,
+ :issues_url,
+ :jira_issue_transition_id,
+ :merge_requests_events,
+ :namespace,
+ :new_issue_url,
+ :notify,
+ :notify_only_broken_builds,
+ :notify_only_broken_pipelines,
+ :password,
+ :priority,
+ :project_key,
+ :project_url,
+ :recipients,
+ :restrict_to_branch,
+ :room,
+ :send_from_committer_email,
+ :server,
+ :server_host,
+ :server_port,
+ :sound,
+ :subdomain,
+ :teamcity_url,
+ :title,
+ :token,
+ :type,
+ :url,
+ :user_key,
+ :username,
+ :webhook
+ ]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
def service_params
dynamic_params = @service.event_channel_names + @service.event_names
- service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params)
+ service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params)
if service_params[:service].is_a?(Hash)
FILTER_BLANK_PARAMS.each do |param|
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 3717c49f272..fbf9a026b10 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -1,11 +1,8 @@
module ToggleAwardEmoji
extend ActiveSupport::Concern
- included do
- before_action :authenticate_user!, only: [:toggle_award_emoji]
- end
-
def toggle_award_emoji
+ authenticate_user!
name = params.require(:name)
if awardable.user_can_award?(current_user, name)
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
index 9e3b9be2ff4..92cb534343e 100644
--- a/app/controllers/concerns/toggle_subscription_action.rb
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -4,13 +4,17 @@ module ToggleSubscriptionAction
def toggle_subscription
return unless current_user
- subscribable_resource.toggle_subscription(current_user)
+ subscribable_resource.toggle_subscription(current_user, subscribable_project)
head :ok
end
private
+ def subscribable_project
+ @project || raise(NotImplementedError)
+ end
+
def subscribable_resource
raise NotImplementedError
end
diff --git a/app/controllers/concerns/workhorse_request.rb b/app/controllers/concerns/workhorse_request.rb
new file mode 100644
index 00000000000..43c0f1b173c
--- /dev/null
+++ b/app/controllers/concerns/workhorse_request.rb
@@ -0,0 +1,13 @@
+module WorkhorseRequest
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :verify_workhorse_api!
+ end
+
+ private
+
+ def verify_workhorse_api!
+ Gitlab::Workhorse.verify_api_request!(request.headers)
+ end
+end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 3da44b9b888..306afb65f10 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -14,12 +14,8 @@ class ConfirmationsController < Devise::ConfirmationsController
if signed_in?(resource_name)
after_sign_in_path_for(resource)
else
- sign_in(resource)
- if signed_in?(resource_name)
- after_sign_in_path_for(resource)
- else
- new_session_path(resource_name)
- end
+ flash[:notice] += " Please sign in."
+ new_session_path(resource_name)
end
end
end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index fa9c6c054f0..7f506db583f 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -1,6 +1,4 @@
class Dashboard::MilestonesController < Dashboard::ApplicationController
- include GlobalMilestones
-
before_action :projects
before_action :milestone, only: [:show]
@@ -17,4 +15,15 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
def show
end
+
+ private
+
+ def milestones
+ @milestones = DashboardMilestone.build_collection(@projects, params)
+ end
+
+ def milestone
+ @milestone = DashboardMilestone.build(@projects, params[:title])
+ render_404 unless @milestone
+ end
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index d425d0f9014..e3933e3d7b1 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -4,6 +4,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def index
@sort = params[:sort]
@todos = @todos.page(params[:page])
+ if @todos.out_of_range? && @todos.total_pages != 0
+ redirect_to url_for(params.merge(page: @todos.total_pages))
+ end
end
def destroy
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 949b4a6c25a..c411c21bb80 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -9,7 +9,7 @@ class Groups::ApplicationController < ApplicationController
def group
unless @group
id = params[:group_id] || params[:id]
- @group = Group.find_by(path: id)
+ @group = Group.find_by_full_path(id)
unless @group && can?(current_user, :read_group, @group)
@group = nil
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 940a3ad20ba..4f273a8d4f0 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,20 +1,20 @@
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
+ include SortingHelper
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
+ @sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
+
@members = @group.group_members
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
+ @members = @members.search(params[:search]) if params[:search].present?
+ @members = @members.sort(@sort)
+ @members = @members.page(params[:page]).per(50)
- if params[:search].present?
- users = @group.users.search(params[:search]).to_a
- @members = @members.where(user_id: users)
- end
-
- @members = @members.order('access_level DESC').page(params[:page]).per(50)
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@group_member = @group.group_members.new
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 29528b2cfaa..587898a8634 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -1,4 +1,6 @@
class Groups::LabelsController < Groups::ApplicationController
+ include ToggleSubscriptionAction
+
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
@@ -69,6 +71,11 @@ class Groups::LabelsController < Groups::ApplicationController
def label
@label ||= @group.labels.find(params[:id])
end
+ alias_method :subscribable_resource, :label
+
+ def subscribable_project
+ nil
+ end
def label_params
params.require(:label).permit(:title, :description, :color)
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 9d5a28e8d4d..0d872c86c8a 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -1,6 +1,4 @@
class Groups::MilestonesController < Groups::ApplicationController
- include GlobalMilestones
-
before_action :group_projects
before_action :milestone, only: [:show, :update]
before_action :authorize_admin_milestones!, only: [:new, :create, :update]
@@ -58,7 +56,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def render_new_with_error(empty_project_ids)
@milestone = Milestone.new(milestone_params)
- @milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids
+ @milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids
render :new
end
@@ -67,10 +65,19 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def milestone_params
- params.require(:milestone).permit(:title, :description, :due_date, :state_event)
+ params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end
def milestone_path(title)
group_milestone_path(@group, title.to_slug.to_s, title: title)
end
+
+ def milestones
+ @milestones = GroupMilestone.build_collection(@group, @projects, params)
+ end
+
+ def milestone
+ @milestone = GroupMilestone.build(@group, @projects, params[:title])
+ render_404 unless @milestone
+ end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index b83c3a872cf..f81237db991 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -42,6 +42,8 @@ class GroupsController < Groups::ApplicationController
@notification_setting = current_user.notification_settings_for(group)
end
+ @nested_groups = group.children
+
setup_projects
respond_to do |format|
@@ -75,13 +77,15 @@ class GroupsController < Groups::ApplicationController
end
def projects
- @projects = @group.projects.page(params[:page])
+ @projects = @group.projects.with_statistics.page(params[:page])
end
def update
if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
+ @group.reset_path!
+
render action: "edit"
end
end
@@ -121,7 +125,11 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(
+ params.require(:group).permit(group_params_ce)
+ end
+
+ def group_params_ce
+ [
:avatar,
:description,
:lfs_enabled,
@@ -131,7 +139,7 @@ class GroupsController < Groups::ApplicationController
:request_access_enabled,
:share_with_group_lock,
:visibility_level
- )
+ ]
end
def load_events
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 4eca278599f..37feff79999 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -6,9 +6,11 @@ class HelpController < ApplicationController
def index
@help_index = File.read(Rails.root.join('doc', 'README.md'))
- # Prefix Markdown links with `help/` unless they already have been
- # See http://rubular.com/r/nwwhzH6Z8X
- @help_index.gsub!(/(\]\()(?!help\/)([^\)\(]+)(\))/, '\1help/\2\3')
+ # Prefix Markdown links with `help/` unless they are external links
+ # See http://rubular.com/r/X3baHTbPO2
+ @help_index.gsub!(%r{(?<delim>\]\()(?!.+://)(?!/)(?<link>[^\)\(]+\))}) do
+ "#{$~[:delim]}#{Gitlab.config.gitlab.relative_url_root}/help/#{$~[:link]}"
+ end
end
def show
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 6ea54744da8..8e42cdf415f 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -2,50 +2,57 @@ class Import::BitbucketController < Import::BaseController
before_action :verify_bitbucket_import_enabled
before_action :bitbucket_auth, except: :callback
- rescue_from OAuth::Error, with: :bitbucket_unauthorized
- rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized
+ rescue_from OAuth2::Error, with: :bitbucket_unauthorized
+ rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
def callback
- request_token = session.delete(:oauth_request_token)
- raise "Session expired!" if request_token.nil?
+ response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
- request_token.symbolize_keys!
-
- access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url)
-
- session[:bitbucket_access_token] = access_token.token
- session[:bitbucket_access_token_secret] = access_token.secret
+ session[:bitbucket_token] = response.token
+ session[:bitbucket_expires_at] = response.expires_at
+ session[:bitbucket_expires_in] = response.expires_in
+ session[:bitbucket_refresh_token] = response.refresh_token
redirect_to status_import_bitbucket_url
end
def status
- @repos = client.projects
- @incompatible_repos = client.incompatible_projects
+ bitbucket_client = Bitbucket::Client.new(credentials)
+ repos = bitbucket_client.repos
+
+ @repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
- @already_added_projects = current_user.created_projects.where(import_type: "bitbucket")
+ @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket')
already_added_projects_names = @already_added_projects.pluck(:import_source)
- @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" }
+ @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status])
- render json: jobs
+ render json: current_user.created_projects
+ .where(import_type: 'bitbucket')
+ .to_json(only: [:id, :import_status])
end
def create
+ bitbucket_client = Bitbucket::Client.new(credentials)
+
@repo_id = params[:repo_id].to_s
- repo = client.project(@repo_id.gsub('___', '/'))
- @project_name = repo['slug']
- @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username'])
+ name = @repo_id.gsub('___', '/')
+ repo = bitbucket_client.repo(name)
+ @project_name = params[:new_name].presence || repo.name
- unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute
- render 'deploy_key' and return
- end
+ repo_owner = repo.owner
+ repo_owner = current_user.username if repo_owner == bitbucket_client.user.username
+ @target_namespace = params[:new_namespace].presence || repo_owner
+
+ namespace = find_or_create_namespace(@target_namespace, current_user)
- if current_user.can?(:create_projects, @target_namespace)
- @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+ if current_user.can?(:create_projects, namespace)
+ # The token in a session can be expired, we need to get most recent one because
+ # Bitbucket::Connection class refreshes it.
+ session[:bitbucket_token] = bitbucket_client.connection.token
+ @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute
else
render 'unauthorized'
end
@@ -54,8 +61,15 @@ class Import::BitbucketController < Import::BaseController
private
def client
- @client ||= Gitlab::BitbucketImport::Client.new(session[:bitbucket_access_token],
- session[:bitbucket_access_token_secret])
+ @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
+ end
+
+ def provider
+ Gitlab::OAuth::Provider.config_for('bitbucket')
+ end
+
+ def options
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
end
def verify_bitbucket_import_enabled
@@ -63,26 +77,23 @@ class Import::BitbucketController < Import::BaseController
end
def bitbucket_auth
- if session[:bitbucket_access_token].blank?
- go_to_bitbucket_for_permissions
- end
+ go_to_bitbucket_for_permissions if session[:bitbucket_token].blank?
end
def go_to_bitbucket_for_permissions
- request_token = client.request_token(callback_import_bitbucket_url)
- session[:oauth_request_token] = request_token
-
- redirect_to client.authorize_url(request_token, callback_import_bitbucket_url)
+ redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url)
end
def bitbucket_unauthorized
go_to_bitbucket_for_permissions
end
- def access_params
+ def credentials
{
- bitbucket_access_token: session[:bitbucket_access_token],
- bitbucket_access_token_secret: session[:bitbucket_access_token_secret]
+ token: session[:bitbucket_token],
+ expires_at: session[:bitbucket_expires_at],
+ expires_in: session[:bitbucket_expires_in],
+ refresh_token: session[:bitbucket_refresh_token]
}
end
end
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
new file mode 100644
index 00000000000..fbd851c64a7
--- /dev/null
+++ b/app/controllers/import/gitea_controller.rb
@@ -0,0 +1,45 @@
+class Import::GiteaController < Import::GithubController
+ def new
+ if session[access_token_key].present? && session[host_key].present?
+ redirect_to status_import_url
+ end
+ end
+
+ def personal_access_token
+ session[host_key] = params[host_key]
+ super
+ end
+
+ def status
+ @gitea_host_url = session[host_key]
+ super
+ end
+
+ private
+
+ def host_key
+ :"#{provider}_host_url"
+ end
+
+ # Overriden methods
+ def provider
+ :gitea
+ end
+
+ # Gitea is not yet an OAuth provider
+ # See https://github.com/go-gitea/gitea/issues/27
+ def logged_in_with_provider?
+ false
+ end
+
+ def provider_auth
+ if session[access_token_key].blank? || session[host_key].blank?
+ redirect_to new_import_gitea_url,
+ alert: 'You need to specify both an Access Token and a Host URL.'
+ end
+ end
+
+ def client_options
+ { host: session[host_key], api_version: 'v1' }
+ end
+end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index ee7d498c59c..53a5981e564 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -1,39 +1,37 @@
class Import::GithubController < Import::BaseController
- before_action :verify_github_import_enabled
- before_action :github_auth, only: [:status, :jobs, :create]
+ before_action :verify_import_enabled
+ before_action :provider_auth, only: [:status, :jobs, :create]
- rescue_from Octokit::Unauthorized, with: :github_unauthorized
-
- helper_method :logged_in_with_github?
+ rescue_from Octokit::Unauthorized, with: :provider_unauthorized
def new
- if logged_in_with_github?
- go_to_github_for_permissions
- elsif session[:github_access_token]
- redirect_to status_import_github_url
+ if logged_in_with_provider?
+ go_to_provider_for_permissions
+ elsif session[access_token_key]
+ redirect_to status_import_url
end
end
def callback
- session[:github_access_token] = client.get_token(params[:code])
- redirect_to status_import_github_url
+ session[access_token_key] = client.get_token(params[:code])
+ redirect_to status_import_url
end
def personal_access_token
- session[:github_access_token] = params[:personal_access_token]
- redirect_to status_import_github_url
+ session[access_token_key] = params[:personal_access_token]
+ redirect_to status_import_url
end
def status
@repos = client.repos
- @already_added_projects = current_user.created_projects.where(import_type: "github")
+ @already_added_projects = current_user.created_projects.where(import_type: provider)
already_added_projects_names = @already_added_projects.pluck(:import_source)
- @repos.reject!{ |repo| already_added_projects_names.include? repo.full_name }
+ @repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "github").to_json(only: [:id, :import_status])
+ jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status])
render json: jobs
end
@@ -44,8 +42,8 @@ class Import::GithubController < Import::BaseController
namespace_path = params[:target_namespace].presence || current_user.namespace_path
@target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
- if current_user.can?(:create_projects, @target_namespace)
- @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute
+ if can?(current_user, :create_projects, @target_namespace)
+ @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
else
render 'unauthorized'
end
@@ -54,34 +52,63 @@ class Import::GithubController < Import::BaseController
private
def client
- @client ||= Gitlab::GithubImport::Client.new(session[:github_access_token])
+ @client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options)
end
- def verify_github_import_enabled
- render_404 unless github_import_enabled?
+ def verify_import_enabled
+ render_404 unless import_enabled?
end
- def github_auth
- if session[:github_access_token].blank?
- go_to_github_for_permissions
- end
+ def go_to_provider_for_permissions
+ redirect_to client.authorize_url(callback_import_url)
end
- def go_to_github_for_permissions
- redirect_to client.authorize_url(callback_import_github_url)
+ def import_enabled?
+ __send__("#{provider}_import_enabled?")
end
- def github_unauthorized
- session[:github_access_token] = nil
- redirect_to new_import_github_url,
- alert: 'Access denied to your GitHub account.'
+ def new_import_url
+ public_send("new_import_#{provider}_url")
end
- def logged_in_with_github?
- current_user.identities.exists?(provider: 'github')
+ def status_import_url
+ public_send("status_import_#{provider}_url")
+ end
+
+ def callback_import_url
+ public_send("callback_import_#{provider}_url")
+ end
+
+ def provider_unauthorized
+ session[access_token_key] = nil
+ redirect_to new_import_url,
+ alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account."
+ end
+
+ def access_token_key
+ :"#{provider}_access_token"
end
def access_params
- { github_access_token: session[:github_access_token] }
+ { github_access_token: session[access_token_key] }
+ end
+
+ # The following methods are overriden in subclasses
+ def provider
+ :github
+ end
+
+ def logged_in_with_provider?
+ current_user.identities.exists?(provider: provider)
+ end
+
+ def provider_auth
+ if session[access_token_key].blank?
+ go_to_provider_for_permissions
+ end
+ end
+
+ def client_options
+ {}
end
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index c736200a104..c2e4d62b50b 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -26,7 +26,7 @@ class JwtController < ApplicationController
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
render_unauthorized unless @authentication_result.success? &&
- (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
+ (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 0f54dfa4efc..2ae4785b12c 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -2,10 +2,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
include Gitlab::GonHelper
include PageLayoutHelper
+ include OauthApplications
before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user!
before_action :add_gon_variables
+ before_action :load_scopes, only: [:index, :create, :edit]
layout 'profile'
diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb
index f193adb46b4..daa51ae41df 100644
--- a/app/controllers/profiles/avatars_controller.rb
+++ b/app/controllers/profiles/avatars_controller.rb
@@ -4,7 +4,6 @@ class Profiles::AvatarsController < Profiles::ApplicationController
@user.remove_avatar!
@user.save
- @user.reset_events_cache
redirect_to profile_path
end
diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb
new file mode 100644
index 00000000000..6a1f468ba5a
--- /dev/null
+++ b/app/controllers/profiles/chat_names_controller.rb
@@ -0,0 +1,64 @@
+class Profiles::ChatNamesController < Profiles::ApplicationController
+ before_action :chat_name_token, only: [:new]
+ before_action :chat_name_params, only: [:new, :create, :deny]
+
+ def index
+ @chat_names = current_user.chat_names
+ end
+
+ def new
+ end
+
+ def create
+ new_chat_name = current_user.chat_names.new(chat_name_params)
+
+ if new_chat_name.save
+ flash[:notice] = "Authorized #{new_chat_name.chat_name}"
+ else
+ flash[:alert] = "Could not authorize chat nickname. Try again!"
+ end
+
+ delete_chat_name_token
+ redirect_to profile_chat_names_path
+ end
+
+ def deny
+ delete_chat_name_token
+
+ flash[:notice] = "Denied authorization of chat nickname #{chat_name_params[:user_name]}."
+
+ redirect_to profile_chat_names_path
+ end
+
+ def destroy
+ @chat_name = chat_names.find(params[:id])
+
+ if @chat_name.destroy
+ flash[:notice] = "Deleted chat nickname: #{@chat_name.chat_name}!"
+ else
+ flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}."
+ end
+
+ redirect_to profile_chat_names_path
+ end
+
+ private
+
+ def delete_chat_name_token
+ chat_name_token.delete
+ end
+
+ def chat_name_params
+ @chat_name_params ||= chat_name_token.get || render_404
+ end
+
+ def chat_name_token
+ return render_404 unless params[:token] || render_404
+
+ @chat_name_token ||= Gitlab::ChatNameToken.new(params[:token])
+ end
+
+ def chat_names
+ @chat_names ||= current_user.chat_names
+ end
+end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 508b82a9a6c..6e007f17913 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -1,8 +1,6 @@
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
- before_action :load_personal_access_tokens, only: :index
-
def index
- @personal_access_token = current_user.personal_access_tokens.build
+ set_index_vars
end
def create
@@ -12,7 +10,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
flash[:personal_access_token] = @personal_access_token.token
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
else
- load_personal_access_tokens
+ set_index_vars
render :index
end
end
@@ -32,10 +30,12 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
private
def personal_access_token_params
- params.require(:personal_access_token).permit(:name, :expires_at)
+ params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
end
- def load_personal_access_tokens
+ def set_index_vars
+ @personal_access_token ||= current_user.personal_access_tokens.build
+ @scopes = Gitlab::Auth::SCOPES
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 9eb75bb3891..18044ca78e2 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
@qr_code = build_qr_code
+ @account_string = account_string
setup_u2f_registration
end
@@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private
def build_qr_code
- issuer = "#{issuer_host} | #{current_user.email}"
- uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
+ uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
end
+ def account_string
+ "#{issuer_host}:#{current_user.email}"
+ end
+
def issuer_host
Gitlab.config.gitlab.host
end
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
new file mode 100644
index 00000000000..d9dfa534669
--- /dev/null
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -0,0 +1,48 @@
+class Projects::AutocompleteSourcesController < Projects::ApplicationController
+ before_action :load_autocomplete_service, except: [:emojis, :members]
+
+ def emojis
+ render json: Gitlab::AwardEmoji.urls
+ end
+
+ def members
+ render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
+ end
+
+ def issues
+ render json: @autocomplete_service.issues
+ end
+
+ def merge_requests
+ render json: @autocomplete_service.merge_requests
+ end
+
+ def labels
+ render json: @autocomplete_service.labels
+ end
+
+ def milestones
+ render json: @autocomplete_service.milestones
+ end
+
+ def commands
+ render json: @autocomplete_service.commands(noteable, params[:type])
+ end
+
+ private
+
+ def load_autocomplete_service
+ @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
+ end
+
+ def noteable
+ case params[:type]
+ when 'Issue'
+ IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
+ when 'MergeRequest'
+ MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
+ when 'Commit'
+ @project.commit(params[:type_id])
+ end
+ end
+end
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index ada7db3c552..53788687076 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -20,7 +20,6 @@ class Projects::AvatarsController < Projects::ApplicationController
@project.remove_avatar!
@project.save
- @project.reset_events_cache
redirect_to edit_project_path(@project)
end
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index f576d0be1fc..863a766a255 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -8,6 +8,9 @@ class Projects::BlameController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@commit.id, @path)
+
+ return render_404 unless @blob
+
@blame_groups = Gitlab::Blame.new(@blob, @commit).groups
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index b78cc6585ba..9940263ae24 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -13,7 +13,6 @@ class Projects::BlobController < Projects::ApplicationController
before_action :assign_blob_vars
before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create]
- before_action :from_merge_request, only: [:edit, :update]
before_action :require_branch_head, only: [:edit, :update]
before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff
@@ -39,14 +38,6 @@ class Projects::BlobController < Projects::ApplicationController
def update
@path = params[:file_path] if params[:file_path].present?
- after_edit_path =
- if from_merge_request && @target_branch == @ref
- diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
- "#file-path-#{hexdigest(@path)}"
- else
- namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
- end
-
create_commit(Files::UpdateService, success_path: after_edit_path,
failure_view: :edit,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
@@ -124,9 +115,14 @@ class Projects::BlobController < Projects::ApplicationController
render_404
end
- def from_merge_request
- # If blob edit was initiated from merge request page
- @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id])
+ def after_edit_path
+ from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid])
+ if from_merge_request && @target_branch == @ref
+ diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
+ "##{hexdigest(@path)}"
+ else
+ namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
+ end
end
def editor_variables
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 2de8ada3e29..89d84809e3a 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -4,7 +4,7 @@ class Projects::BranchesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:new, :create, :destroy]
+ before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
def index
@sort = params[:sort].presence || sort_value_name
@@ -36,7 +36,7 @@ class Projects::BranchesController < Projects::ApplicationController
execute(branch_name, ref)
if params[:issue_iid]
- issue = @project.issues.find_by(iid: params[:issue_iid])
+ issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid])
SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
end
@@ -62,6 +62,13 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
+ def destroy_all_merged
+ DeleteMergedBranchesService.new(@project, current_user).async_execute
+
+ redirect_to namespace_project_branches_path(@project.namespace, @project),
+ notice: 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.'
+ end
+
private
def ref
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index fbe391fc58c..9b45ed6b6af 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -94,7 +94,7 @@ class Projects::BuildsController < Projects::ApplicationController
private
def build
- @build ||= project.builds.find_by!(id: params[:id])
+ @build ||= project.builds.find_by!(id: params[:id]).present(user: current_user)
end
def build_path(build)
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index cdfc1ba7b92..bfc59bcc862 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -8,13 +8,10 @@ class Projects::CommitController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
- before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
+ before_action :authorize_download_code!
before_action :authorize_read_pipeline!, only: [:pipelines]
- before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
- before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines]
- before_action :define_status_vars, only: [:show, :builds, :pipelines]
+ before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
@@ -35,25 +32,6 @@ class Projects::CommitController < Projects::ApplicationController
def pipelines
end
- def builds
- end
-
- def cancel_builds
- ci_builds.running_or_pending.each(&:cancel)
-
- redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
- end
-
- def retry_builds
- ci_builds.latest.failed.each do |build|
- if build.retryable?
- Ci::Build.retry(build, current_user)
- end
- end
-
- redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
- end
-
def branches
@branches = @project.repository.branch_names_contains(commit.id)
@tags = @project.repository.tag_names_contains(commit.id)
@@ -65,7 +43,7 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @target_branch.blank?
- create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.",
+ create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
success_path: successful_change_path, failure_path: failed_change_path)
end
@@ -74,36 +52,30 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @target_branch.blank?
- create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.",
+ create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
success_path: successful_change_path, failure_path: failed_change_path)
end
private
def successful_change_path
- return referenced_merge_request_url if @commit.merged_merge_request
-
- namespace_project_commits_url(@project.namespace, @project, @target_branch)
+ referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch)
end
def failed_change_path
- return referenced_merge_request_url if @commit.merged_merge_request
-
- namespace_project_commit_url(@project.namespace, @project, params[:id])
+ referenced_merge_request_url || namespace_project_commit_url(@project.namespace, @project, params[:id])
end
def referenced_merge_request_url
- namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request)
+ if merge_request = @commit.merged_merge_request(current_user)
+ namespace_project_merge_request_url(@project.namespace, @project, merge_request)
+ end
end
def commit
@noteable = @commit ||= @project.commit(params[:id])
end
- def ci_builds
- @ci_builds ||= Ci::Build.where(pipeline: pipelines)
- end
-
def define_commit_vars
return git_not_found! unless commit
@@ -133,12 +105,6 @@ class Projects::CommitController < Projects::ApplicationController
}
end
- def define_status_vars
- @ci_pipelines = project.pipelines.where(sha: commit.sha)
- @statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant
- @builds = Ci::Build.where(pipeline: @ci_pipelines).relevant
- end
-
def assign_change_commit_vars(mr_source_branch)
@commit = project.commit(params[:id])
@target_branch = params[:target_branch]
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index aba87b6144b..ad92f05a42d 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -21,7 +21,7 @@ class Projects::CommitsController < Projects::ApplicationController
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
- @merge_request = @project.merge_requests.opened.
+ @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
respond_to do |format|
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index bee3d56076c..d32966645c8 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -25,8 +25,17 @@ class Projects::CompareController < Projects::ApplicationController
end
def create
- redirect_to namespace_project_compare_path(@project.namespace, @project,
+ if params[:from].blank? || params[:to].blank?
+ flash[:alert] = "You must select from and to branches"
+ from_to_vars = {
+ from: params[:from].presence,
+ to: params[:to].presence
+ }
+ redirect_to namespace_project_compare_index_path(@project.namespace, @project, from_to_vars)
+ else
+ redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to])
+ end
end
private
@@ -53,7 +62,7 @@ class Projects::CompareController < Projects::ApplicationController
end
def merge_request
- @merge_request ||= @project.merge_requests.opened.
+ @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
end
end
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
new file mode 100644
index 00000000000..b69d46f2c41
--- /dev/null
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -0,0 +1,61 @@
+module Projects
+ module CycleAnalytics
+ class EventsController < Projects::ApplicationController
+ include CycleAnalyticsParams
+
+ before_action :authorize_read_cycle_analytics!
+ before_action :authorize_read_build!, only: [:test, :staging]
+ before_action :authorize_read_issue!, only: [:issue, :production]
+ before_action :authorize_read_merge_request!, only: [:code, :review]
+
+ def issue
+ render_events(cycle_analytics[:issue].events)
+ end
+
+ def plan
+ render_events(cycle_analytics[:plan].events)
+ end
+
+ def code
+ render_events(cycle_analytics[:code].events)
+ end
+
+ def test
+ options(events_params)[:branch] = events_params[:branch_name]
+
+ render_events(cycle_analytics[:test].events)
+ end
+
+ def review
+ render_events(cycle_analytics[:review].events)
+ end
+
+ def staging
+ render_events(cycle_analytics[:staging].events)
+ end
+
+ def production
+ render_events(cycle_analytics[:production].events)
+ end
+
+ private
+
+ def render_events(events)
+ respond_to do |format|
+ format.html
+ format.json { render json: { events: events } }
+ end
+ end
+
+ def cycle_analytics
+ @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params))
+ end
+
+ def events_params
+ return {} unless params[:events].present?
+
+ params[:events].permit(:start_date, :branch_name)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index 16a7b1fc6e2..88ac3ad046b 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -1,11 +1,14 @@
class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TextHelper
+ include CycleAnalyticsParams
before_action :authorize_read_cycle_analytics!
def show
- @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date)
+ @cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params))
+
+ @cycle_analytics_no_data = @cycle_analytics.no_stats?
respond_to do |format|
format.html
@@ -15,53 +18,17 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
private
- def parse_start_date
- case cycle_analytics_params[:start_date]
- when '30' then 30.days.ago
- when '90' then 90.days.ago
- else 90.days.ago
- end
- end
-
def cycle_analytics_params
return {} unless params[:cycle_analytics].present?
- { start_date: params[:cycle_analytics][:start_date] }
+ params[:cycle_analytics].permit(:start_date)
end
def cycle_analytics_json
- cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"],
- [:plan, "Plan", "Time before an issue starts implementation"],
- [:code, "Code", "Time until first merge request"],
- [:test, "Test", "Total test time for all commits/merges"],
- [:review, "Review", "Time between merge request creation and merge/close"],
- [:staging, "Staging", "From merge request merge until deploy to production"],
- [:production, "Production", "From issue creation until deploy to production"]]
-
- stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|
- value = @cycle_analytics.send(stage_method).presence
-
- stats << {
- title: stage_text,
- description: stage_description,
- value: value && !value.zero? ? distance_of_time_in_words(value) : nil
- }
- stats
- end
-
- issues = @cycle_analytics.summary.new_issues
- commits = @cycle_analytics.summary.commits
- deploys = @cycle_analytics.summary.deploys
-
- summary = [
- { title: "New Issue".pluralize(issues), value: issues },
- { title: "Commit".pluralize(commits), value: commits },
- { title: "Deploy".pluralize(deploys), value: deploys }
- ]
-
{
- summary: summary,
- stats: stats
+ summary: @cycle_analytics.summary,
+ stats: @cycle_analytics.stats,
+ permissions: @cycle_analytics.permissions(user: current_user)
}
end
end
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 529e0aa2d33..b094491e006 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -16,7 +16,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
- @key = DeployKey.new(deploy_key_params)
+ @key = DeployKey.new(deploy_key_params.merge(user: current_user))
set_index_vars
if @key.valid? && @project.deploy_keys << @key
@@ -53,6 +53,6 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def deploy_key_params
- params.require(:deploy_key).permit(:key, :title)
+ params.require(:deploy_key).permit(:key, :title, :can_push)
end
end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index d174e1145a7..1349b015a63 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -5,9 +5,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
before_action :authorize_resolve_discussion!
def resolve
- discussion.resolve!(current_user)
-
- MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
+ Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion)
render json: {
resolved_by: discussion.resolved_by.try(:name),
@@ -26,7 +24,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
private
def merge_request
- @merge_request ||= @project.merge_requests.find_by!(iid: params[:merge_request_id])
+ @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id])
end
def discussion
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index bc66823dfc4..a203efc62b8 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -4,17 +4,22 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update]
- before_action :environment, only: [:show, :edit, :update, :stop]
+ before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
+ before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize]
+ before_action :verify_api_request!, only: :terminal_websocket_authorize
def index
@scope = params[:scope]
- @all_environments = project.environments
- @environments =
- if @scope == 'stopped'
- @all_environments.stopped
- else
- @all_environments.available
+ @environments = project.environments
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: EnvironmentSerializer
+ .new(project: @project, user: current_user)
+ .represent(@environments)
end
+ end
end
def show
@@ -57,8 +62,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ def terminal
+ # Currently, this acts as a hint to load the terminal details into the cache
+ # if they aren't there already. In the future, users will need these details
+ # to choose between terminals to connect to.
+ @terminals = environment.terminals
+ end
+
+ # GET .../terminal.ws : implemented in gitlab-workhorse
+ def terminal_websocket_authorize
+ # Just return the first terminal for now. If the list is in the process of
+ # being looked up, this may result in a 404 response, so the frontend
+ # should retry those errors
+ terminal = environment.terminals.try(:first)
+ if terminal
+ set_workhorse_internal_api_content_type
+ render json: Gitlab::Workhorse.terminal_websocket(terminal)
+ else
+ render text: 'Not found', status: 404
+ end
+ end
+
private
+ def verify_api_request!
+ Gitlab::Workhorse.verify_api_request!(request.headers)
+ end
+
def environment_params
params.require(:environment).permit(:name, :external_url)
end
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index ade01c706a7..ba46e2528e6 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -4,6 +4,7 @@ class Projects::ForksController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
+ before_action :authenticate_user!, only: [:new, :create]
def index
base_query = project.forks.includes(:creator)
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 3f41916e6d3..8714349e27f 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -18,6 +18,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController
private
+ def download_request?
+ raise NotImplementedError
+ end
+
+ def upload_request?
+ raise NotImplementedError
+ end
+
def authenticate_user
@authentication_result = Gitlab::Auth::Result.new
@@ -130,10 +138,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController
authentication_result.ci?(project)
end
- def lfs_deploy_token?
- authentication_result.lfs_deploy_token?(project)
- end
-
def authentication_has_download_access?
has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code)
end
@@ -149,8 +153,4 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def authentication_project
authentication_result.project
end
-
- def verify_workhorse_api!
- Gitlab::Workhorse.verify_api_request!(request.headers)
- end
end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 13caeb42d40..9184dcccac5 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -1,7 +1,5 @@
-# This file should be identical in GitLab Community Edition and Enterprise Edition
-
class Projects::GitHttpController < Projects::GitHttpClientController
- before_action :verify_workhorse_api!
+ include WorkhorseRequest
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
@@ -67,14 +65,18 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def render_denied
- if user && user.can?(:read_project, project)
- render plain: 'Access denied', status: :forbidden
+ if user && can?(user, :read_project, project)
+ render plain: access_denied_message, status: :forbidden
else
# Do not leak information about project existence
render_not_found
end
end
+ def access_denied_message
+ 'Access denied'
+ end
+
def upload_pack_allowed?
return false unless Gitlab.config.gitlab_shell.upload_pack
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 9eaf26a0dbf..66b7bdbd988 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
before_action :authorize_admin_project_member!, only: [:update]
def index
- @group_links = project.project_group_links.all
-
- @skip_groups = @group_links.pluck(:group_id)
- @skip_groups << project.namespace_id unless project.personal?
+ redirect_to namespace_project_settings_members_path
end
def create
@@ -25,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
flash[:alert] = 'Please select a group.'
end
- redirect_to namespace_project_group_links_path(project.namespace, project)
+ redirect_to namespace_project_settings_members_path(project.namespace, project)
end
def update
@@ -39,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format|
format.html do
- redirect_to namespace_project_group_links_path(project.namespace, project)
+ redirect_to namespace_project_settings_members_path(project.namespace, project)
end
format.js { head :ok }
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 0ae8ff98009..b668a9331e7 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -6,21 +6,15 @@ class Projects::HooksController < Projects::ApplicationController
layout "project_settings"
- def index
- @hooks = @project.hooks
- @hook = ProjectHook.new
- end
-
def create
@hook = @project.hooks.new(hook_params)
@hook.save
- if @hook.valid?
- redirect_to namespace_project_hooks_path(@project.namespace, @project)
- else
+ unless @hook.valid?
@hooks = @project.hooks.select(&:persisted?)
- render :index
+ flash[:alert] = @hook.errors.full_messages.join.html_safe
end
+ redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
def test
@@ -44,7 +38,7 @@ class Projects::HooksController < Projects::ApplicationController
def destroy
hook.destroy
- redirect_to namespace_project_hooks_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
private
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 3f1a1d1c511..8472ceca329 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -25,11 +25,26 @@ class Projects::IssuesController < Projects::ApplicationController
def index
@issues = issues_collection
@issues = @issues.page(params[:page])
+ if @issues.out_of_range? && @issues.total_pages != 0
+ return redirect_to url_for(params.merge(page: @issues.total_pages))
+ end
if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
end
+ @users = []
+
+ if params[:assignee_id].present?
+ assignee = User.find_by_id(params[:assignee_id])
+ @users.push(assignee) if assignee
+ end
+
+ if params[:author_id].present?
+ author = User.find_by_id(params[:author_id])
+ @users.push(author) if author
+ end
+
respond_to do |format|
format.html
format.atom { render layout: false }
@@ -46,8 +61,9 @@ class Projects::IssuesController < Projects::ApplicationController
params[:issue] ||= ActionController::Parameters.new(
assignee_id: ""
)
+ build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
+ @issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute
- @issue = @noteable = @project.issues.new(issue_params)
respond_with(@issue)
end
@@ -69,13 +85,15 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
- render json: @issue.to_json(include: [:milestone, :labels])
+ render json: IssueSerializer.new.represent(@issue)
end
end
end
def create
- @issue = Issues::CreateService.new(project, current_user, issue_params.merge(request: request)).execute
+ extra_params = { request: request,
+ merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
+ @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
respond_to do |format|
format.html do
@@ -169,6 +187,14 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :awardable, :issue
alias_method :spammable, :issue
+ def merge_request_for_resolving_discussions
+ return unless merge_request_iid = params[:merge_request_for_resolving_discussions]
+
+ @merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id).
+ execute.
+ find_by(iid: merge_request_iid)
+ end
+
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 42fd09e9b7e..824ed7be73e 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -3,7 +3,7 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
- before_action :find_labels, only: [:index, :set_priorities, :remove_priority]
+ before_action :find_labels, only: [:index, :set_priorities, :remove_priority, :toggle_subscription]
before_action :authorize_read_label!
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority,
@@ -123,7 +123,10 @@ class Projects::LabelsController < Projects::ApplicationController
def label
@label ||= @project.labels.find(params[:id])
end
- alias_method :subscribable_resource, :label
+
+ def subscribable_resource
+ @available_labels.find(params[:id])
+ end
def find_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index ece49dcd922..440259b643c 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -1,8 +1,7 @@
class Projects::LfsApiController < Projects::GitHttpClientController
- include LfsHelper
+ include LfsRequest
- before_action :require_lfs_enabled!
- before_action :lfs_check_access!, except: [:deprecated]
+ skip_before_action :lfs_check_access!, only: [:deprecated]
def batch
unless objects.present?
@@ -31,8 +30,12 @@ class Projects::LfsApiController < Projects::GitHttpClientController
private
- def objects
- @objects ||= (params[:objects] || []).to_a
+ def download_request?
+ params[:operation] == 'download'
+ end
+
+ def upload_request?
+ params[:operation] == 'upload'
end
def existing_oids
@@ -83,12 +86,4 @@ class Projects::LfsApiController < Projects::GitHttpClientController
}
}
end
-
- def download_request?
- params[:operation] == 'download'
- end
-
- def upload_request?
- params[:operation] == 'upload'
- end
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 9005b104e90..32759672b6c 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -1,9 +1,8 @@
class Projects::LfsStorageController < Projects::GitHttpClientController
- include LfsHelper
+ include LfsRequest
+ include WorkhorseRequest
- before_action :require_lfs_enabled!
- before_action :lfs_check_access!
- before_action :verify_workhorse_api!, only: [:upload_authorize]
+ skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize]
def download
lfs_object = LfsObject.find_by_oid(oid)
diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb
new file mode 100644
index 00000000000..01d99c7df35
--- /dev/null
+++ b/app/controllers/projects/mattermosts_controller.rb
@@ -0,0 +1,43 @@
+class Projects::MattermostsController < Projects::ApplicationController
+ include TriggersHelper
+ include ActionView::Helpers::AssetUrlHelper
+
+ layout 'project_settings'
+
+ before_action :authorize_admin_project!
+ before_action :service
+ before_action :teams, only: [:new]
+
+ def new
+ end
+
+ def create
+ result, message = @service.configure(current_user, configure_params)
+
+ if result
+ flash[:notice] = 'This service is now configured'
+ redirect_to edit_namespace_project_service_path(
+ @project.namespace, @project, service)
+ else
+ flash[:alert] = message || 'Failed to configure service'
+ redirect_to new_namespace_project_mattermost_path(
+ @project.namespace, @project)
+ end
+ end
+
+ private
+
+ def configure_params
+ params.require(:mattermost).permit(:trigger, :team_id).merge(
+ url: service_trigger_url(@service),
+ icon_url: asset_url('slash-command-logo.png'))
+ end
+
+ def teams
+ @teams ||= @service.list_teams(current_user)
+ end
+
+ def service
+ @service ||= @project.find_or_initialize_service('mattermost_slash_commands')
+ end
+end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index ccba37c9c5c..bdf8d2ce628 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,10 +9,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
- :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check,
+ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
:ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
- before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
+ before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs]
@@ -38,7 +38,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def index
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
- @merge_requests = @merge_requests.preload(:target_project)
+ if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
+ return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
+ end
if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
@@ -61,7 +63,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.html { define_discussion_vars }
format.json do
- render json: @merge_request
+ render json: MergeRequestSerializer.new.represent(@merge_request)
end
format.patch do
@@ -83,12 +85,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff =
if params[:diff_id]
- @merge_request.merge_request_diffs.find(params[:diff_id])
+ @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else
@merge_request.merge_request_diff
end
- @merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@@ -96,7 +98,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
- render_404
+ @start_sha = @merge_request_diff.head_commit_sha
+ @start_version = @merge_request_diff
end
end
@@ -202,17 +205,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
- def builds
- respond_to do |format|
- format.html do
- define_discussion_vars
-
- render 'show'
- end
- format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } }
- end
- end
-
def pipelines
@pipelines = @merge_request.all_pipelines
@@ -303,9 +295,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def cancel_merge_when_build_succeeds
- return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ return access_denied!
+ end
- MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request)
+ MergeRequests::MergeWhenPipelineSucceedsService
+ .new(@project, current_user)
+ .cancel(@merge_request)
end
def merge
@@ -326,16 +322,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present?
- unless @merge_request.pipeline
+ unless @merge_request.head_pipeline
@status = :failed
return
end
- if @merge_request.pipeline.active?
- MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
- .execute(@merge_request)
+ if @merge_request.head_pipeline.active?
+ MergeRequests::MergeWhenPipelineSucceedsService
+ .new(@project, current_user, merge_params)
+ .execute(@merge_request)
+
@status = :merge_when_build_succeeds
- elsif @merge_request.pipeline.success?
+ elsif @merge_request.head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@@ -349,6 +347,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def merge_widget_refresh
+ if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged'
+ @status = :success
+ elsif merge_request.merge_when_build_succeeds
+ @status = :merge_when_build_succeeds
+ end
+
+ render 'merge'
+ end
+
def branch_from
# This is always source
@source_project = @merge_request.nil? ? @project : @merge_request.source_project
@@ -399,7 +407,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def ci_status
- pipeline = @merge_request.pipeline
+ pipeline = @merge_request.head_pipeline
+
if pipeline
status = pipeline.status
coverage = pipeline.try(:coverage)
@@ -410,15 +419,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
else
ci_service = @merge_request.source_project.try(:ci_service)
status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
-
- if ci_service.respond_to?(:commit_coverage)
- coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch)
- end
end
response = {
title: merge_request.title,
- sha: merge_request.diff_head_commit.short_id,
+ sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status,
coverage: coverage
}
@@ -492,7 +497,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def validates_merge_request
# Show git not found page
# if there is no saved commits between source & target branch
- if @merge_request.commits.blank?
+ if @merge_request.has_no_commits?
# and if target branch doesn't exist
return invalid_mr unless @merge_request.target_branch_exists?
end
@@ -500,13 +505,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_show_vars
@noteable = @merge_request
- @commits_count = @merge_request.commits.count
+ @commits_count = @merge_request.commits_count
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
end
+ labels
define_pipelines_vars
end
@@ -534,7 +540,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def define_widget_vars
- @pipeline = @merge_request.pipeline
+ @pipeline = @merge_request.head_pipeline
end
def define_commit_vars
@@ -563,11 +569,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_pipelines_vars
@pipelines = @merge_request.all_pipelines
-
- if @pipelines.present?
- @pipeline = @pipelines.first
- @statuses = @pipeline.statuses.relevant
- end
+ @pipeline = @merge_request.head_pipeline
+ @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0
end
def define_new_vars
@@ -634,7 +637,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_when_build_succeeds_active?
params[:merge_when_build_succeeds].present? &&
- @merge_request.pipeline && @merge_request.pipeline.active?
+ @merge_request.head_pipeline && @merge_request.head_pipeline.active?
end
def build_merge_request
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index ff63f22cb5b..be52b0fa7cf 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -112,6 +112,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def milestone_params
- params.require(:milestone).permit(:title, :description, :due_date, :state_event)
+ params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 0948ad21649..c5d93ce25bc 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController
end
def create
- @note = Notes::CreateService.new(project, current_user, note_params).execute
+ create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha])
+ @note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
@@ -146,24 +147,26 @@ class Projects::NotesController < Projects::ApplicationController
end
def note_json(note)
+ attrs = {
+ award: false,
+ id: note.id
+ }
+
if note.is_a?(AwardEmoji)
- {
+ attrs.merge!(
valid: note.valid?,
award: true,
- id: note.id,
name: note.name
- }
+ )
elsif note.persisted?
Banzai::NoteRenderer.render([note], @project, current_user)
- attrs = {
+ attrs.merge!(
valid: true,
- id: note.id,
discussion_id: note.discussion_id,
html: note_html(note),
- award: false,
note: note.note
- }
+ )
if note.diff_note?
discussion = note.to_discussion
@@ -188,15 +191,15 @@ class Projects::NotesController < Projects::ApplicationController
attrs[:original_discussion_id] = note.original_discussion_id
end
end
-
- attrs
else
- {
+ attrs.merge!(
valid: false,
- award: false,
errors: note.errors
- }
+ )
end
+
+ attrs[:commands_changes] = note.commands_changes unless attrs[:award]
+ attrs
end
def authorize_admin_note!
@@ -215,6 +218,6 @@ class Projects::NotesController < Projects::ApplicationController
end
def find_current_user_notes
- @notes = NotesFinder.new.execute(project, current_user, params)
+ @notes = NotesFinder.new(project, current_user, params).execute.inc_author
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 371cc3787fb..84451257b98 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -1,16 +1,39 @@
class Projects::PipelinesController < Projects::ApplicationController
before_action :pipeline, except: [:index, :new, :create]
- before_action :commit, only: [:show]
+ before_action :commit, only: [:show, :builds]
before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
def index
@scope = params[:scope]
- @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
+ @pipelines = PipelinesFinder
+ .new(project)
+ .execute(scope: @scope)
+ .page(params[:page])
+ .per(30)
- @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
- @pipelines_count = PipelinesFinder.new(project).execute.count
+ @running_or_pending_count = PipelinesFinder
+ .new(project).execute(scope: 'running').count
+
+ @pipelines_count = PipelinesFinder
+ .new(project).execute.count
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ pipelines: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .with_pagination(request, response)
+ .represent(@pipelines),
+ count: {
+ all: @pipelines_count,
+ running_or_pending: @running_or_pending_count
+ }
+ }
+ end
+ end
end
def new
@@ -18,7 +41,9 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create
- @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false)
+ @pipeline = Ci::CreatePipelineService
+ .new(project, current_user, create_params)
+ .execute(ignore_skip_ci: true, save_on_errors: false)
unless @pipeline.persisted?
render 'new'
return
@@ -30,6 +55,23 @@ class Projects::PipelinesController < Projects::ApplicationController
def show
end
+ def builds
+ respond_to do |format|
+ format.html do
+ render 'show'
+ end
+ end
+ end
+
+ def stage
+ @stage = pipeline.stage(params[:stage])
+ return not_found unless @stage
+
+ respond_to do |format|
+ format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
+ end
+ end
+
def retry
pipeline.retry_failed(current_user)
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 9136633b87a..53ce23221ed 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -17,7 +17,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project)
else
- render 'index'
+ render 'show'
end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 699a56ae2f8..6e158e685e9 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,33 +1,19 @@
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
+ include SortingHelper
# Authorize
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
- @group_links = @project.project_group_links
-
- @project_members = @project.project_members
- @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
-
- if params[:search].present?
- users = @project.users.search(params[:search]).to_a
- @project_members = @project_members.where(user_id: users)
-
- @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
- end
-
- @project_members = @project_members.order(access_level: :desc).page(params[:page])
-
- @requesters = AccessRequestsFinder.new(@project).execute(current_user)
-
- @project_member = @project.project_members.new
+ sort = params[:sort].presence || sort_value_name
+ redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
end
def create
status = Members::CreateService.new(@project, current_user, params).execute
- redirect_url = namespace_project_project_members_path(@project.namespace, @project)
+ redirect_url = namespace_project_settings_members_path(@project.namespace, @project)
if status
redirect_to redirect_url, notice: 'Users were successfully added.'
@@ -50,14 +36,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
respond_to do |format|
format.html do
- redirect_to namespace_project_project_members_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_members_path(@project.namespace, @project)
end
format.js { head :ok }
end
end
def resend_invite
- redirect_path = namespace_project_project_members_path(@project.namespace, @project)
+ redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
@project_member = @project.project_members.find(params[:id])
@@ -80,7 +66,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
return render_404
end
- redirect_to(namespace_project_project_members_path(project.namespace, project),
+ redirect_to(namespace_project_settings_members_path(project.namespace, project),
notice: notice)
end
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 0825a4311cb..2c097cb4d8d 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController
end
def update
- release.update_attributes(release_params)
+ # Release belongs to Tag which is not active record object,
+ # it exists only to save a description to each Tag.
+ # If description is empty we should destroy the existing record.
+ if release_params[:description].present?
+ release.update_attributes(release_params)
+ else
+ release.destroy
+ end
redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 97e6e9471e0..17cb1d5be24 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -9,11 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings"
- def index
- @project.build_missing_services
- @services = @project.services.visible.reload
- end
-
def edit
end
@@ -29,6 +24,8 @@ class Projects::ServicesController < Projects::ApplicationController
end
def test
+ return render_404 unless @service.can_test?
+
data = @service.test_data(project, current_user)
outcome = @service.test(data)
@@ -46,6 +43,6 @@ class Projects::ServicesController < Projects::ApplicationController
private
def service
- @service ||= @project.services.find { |service| service.to_param == params[:id] }
+ @service ||= @project.find_or_initialize_service(params[:id])
end
end
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
new file mode 100644
index 00000000000..fb2a4837735
--- /dev/null
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -0,0 +1,18 @@
+module Projects
+ module Settings
+ class IntegrationsController < Projects::ApplicationController
+ include ServiceParams
+
+ before_action :authorize_admin_project!
+ layout "project_settings"
+
+ def show
+ @hooks = @project.hooks
+ @hook = ProjectHook.new
+
+ # Services
+ @services = @project.find_or_initialize_services
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb
new file mode 100644
index 00000000000..5735e281f66
--- /dev/null
+++ b/app/controllers/projects/settings/members_controller.rb
@@ -0,0 +1,55 @@
+module Projects
+ module Settings
+ class MembersController < Projects::ApplicationController
+ include SortingHelper
+
+ def show
+ @sort = params[:sort].presence || sort_value_name
+ @group_links = @project.project_group_links
+
+ @project_members = @project.project_members
+ @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
+
+ group = @project.group
+
+ # group links
+ @group_links = @project.project_group_links.all
+
+ @skip_groups = @group_links.pluck(:group_id)
+ @skip_groups << @project.namespace_id unless @project.personal?
+
+ if group
+ # We need `.where.not(user_id: nil)` here otherwise when a group has an
+ # invitee, it would make the following query return 0 rows since a NULL
+ # user_id would be present in the subquery
+ # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+ group_members = MembersFinder.new(@project_members, group).execute(current_user)
+ end
+
+ if params[:search].present?
+ user_ids = @project.users.search(params[:search]).select(:id)
+ @project_members = @project_members.where(user_id: user_ids)
+
+ if group_members
+ user_ids = group.users.search(params[:search]).select(:id)
+ group_members = group_members.where(user_id: user_ids)
+ end
+
+ @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
+ end
+
+ wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
+ wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
+
+ @project_members = Member.
+ where(wheres.join(' OR ')).
+ sort(@sort).
+ page(params[:page])
+
+ @requesters = AccessRequestsFinder.new(@project).execute(current_user)
+
+ @project_member = @project.project_members.new
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index e290a0eadda..02a97c1c574 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -19,11 +19,16 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
- @snippets = SnippetsFinder.new.execute(current_user, {
+ @snippets = SnippetsFinder.new.execute(
+ current_user,
filter: :by_project,
- project: @project
- })
+ project: @project,
+ scope: params[:scope]
+ )
@snippets = @snippets.page(params[:page])
+ if @snippets.out_of_range? && @snippets.total_pages != 0
+ redirect_to namespace_project_snippets_path(page: @snippets.total_pages)
+ end
end
def new
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 953091492ae..e2d9d5ed460 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -8,7 +8,7 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def index
- params[:sort] = params[:sort].presence || 'name'
+ params[:sort] = params[:sort].presence || sort_value_recently_updated
@sort = params[:sort]
@tags = TagsFinder.new(@repository, params).execute
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index 5685d0f4e7c..a41fcb85c40 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -16,15 +16,9 @@ class Projects::TodosController < Projects::ApplicationController
@issuable ||= begin
case params[:issuable_type]
when "issue"
- issue = @project.issues.find(params[:issuable_id])
-
- if can?(current_user, :read_issue, issue)
- issue
- else
- render_404
- end
+ IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
when "merge_request"
- @project.merge_requests.find(params[:issuable_id])
+ MergeRequestsFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
end
end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 177ccf5eec9..c3353446fd1 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -115,6 +115,8 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
+
+ @sidebar_wiki_pages = @project_wiki.pages.first(15)
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a8a18b4fa16..444ff837bb3 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -42,19 +42,16 @@ class ProjectsController < Projects::ApplicationController
end
def update
- status = ::Projects::UpdateService.new(@project, current_user, project_params).execute
+ result = ::Projects::UpdateService.new(@project, current_user, project_params).execute
# Refresh the repo in case anything changed
- @repository = project.repository
+ @repository = @project.repository
respond_to do |format|
- if status
+ if result[:status] == :success
flash[:notice] = "Project '#{@project.name}' was successfully updated."
format.html do
- redirect_to(
- edit_project_path(@project),
- notice: "Project '#{@project.name}' was successfully updated."
- )
+ redirect_to(edit_project_path(@project))
end
else
format.html { render 'edit' }
@@ -127,39 +124,6 @@ class ProjectsController < Projects::ApplicationController
redirect_to edit_project_path(@project), alert: ex.message
end
- def autocomplete_sources
- noteable =
- case params[:type]
- when 'Issue'
- IssuesFinder.new(current_user, project_id: @project.id).
- execute.find_by(iid: params[:type_id])
- when 'MergeRequest'
- MergeRequestsFinder.new(current_user, project_id: @project.id).
- execute.find_by(iid: params[:type_id])
- when 'Commit'
- @project.commit(params[:type_id])
- else
- nil
- end
-
- autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
- participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
-
- @suggestions = {
- emojis: Gitlab::AwardEmoji.urls,
- issues: autocomplete.issues,
- milestones: autocomplete.milestones,
- mergerequests: autocomplete.merge_requests,
- labels: autocomplete.labels,
- members: participants,
- commands: autocomplete.commands(noteable, params[:type])
- }
-
- respond_to do |format|
- format.json { render json: @suggestions }
- end
- end
-
def new_issue_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 3327f4f2b87..bf27f3d4d51 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -7,17 +7,17 @@ class RegistrationsController < Devise::RegistrationsController
end
def create
- if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
- # To avoid duplicate form fields on the login page, the registration form
- # names fields using `new_user`, but Devise still wants the params in
- # `user`.
- if params["new_#{resource_name}"].present? && params[resource_name].blank?
- params[resource_name] = params.delete(:"new_#{resource_name}")
- end
+ # To avoid duplicate form fields on the login page, the registration form
+ # names fields using `new_user`, but Devise still wants the params in
+ # `user`.
+ if params["new_#{resource_name}"].present? && params[resource_name].blank?
+ params[resource_name] = params.delete(:"new_#{resource_name}")
+ end
+ if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super
else
- flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code."
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.'
flash.delete :recaptcha_error
render action: 'new'
end
@@ -27,7 +27,10 @@ class RegistrationsController < Devise::RegistrationsController
DeleteUserService.new(current_user).execute(current_user)
respond_to do |format|
- format.html { redirect_to new_user_session_path, notice: "Account successfully removed." }
+ format.html do
+ session.try(:destroy)
+ redirect_to new_user_session_path, notice: "Account successfully removed."
+ end
end
end
@@ -54,7 +57,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def sign_up_params
- params.require(:user).permit(:username, :email, :name, :password, :password_confirmation)
+ params.require(:user).permit(:username, :email, :email_confirmation, :name, :password)
end
def resource_name
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index b666aa01d6b..6576ebd5235 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -45,6 +45,8 @@ class SearchController < ApplicationController
end
@search_objects = @search_results.objects(@scope, params[:page])
+
+ check_single_commit_result
end
def autocomplete
@@ -59,4 +61,16 @@ class SearchController < ApplicationController
render json: search_autocomplete_opts(term).to_json
end
+
+ private
+
+ def check_single_commit_result
+ if @search_results.single_commit_result?
+ only_commit = @search_results.objects('commits').first
+ query = params[:search].strip.downcase
+ found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query)
+
+ redirect_to namespace_project_commit_path(@project.namespace, @project, only_commit) if found_by_commit_sha
+ end
+ end
end
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 3085ff33aba..04c36b3ebfe 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -12,7 +12,7 @@ class SentNotificationsController < ApplicationController
def unsubscribe_and_redirect
noteable = @sent_notification.noteable
- noteable.unsubscribe(@sent_notification.recipient)
+ noteable.unsubscribe(@sent_notification.recipient, @sent_notification.project)
flash[:notice] = "You have been unsubscribed from this thread."
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 5d7ecfeacf4..93a180b9036 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -31,10 +31,18 @@ class SessionsController < Devise::SessionsController
resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil)
end
+ # hide the signed-in notification
+ flash[:notice] = nil
log_audit_event(current_user, with: authentication_method)
end
end
+ def destroy
+ super
+ # hide the signed_out notice
+ flash[:notice] = nil
+ end
+
private
# Handle an "initial setup" state, where there's only one user, it's an admin,
@@ -106,7 +114,7 @@ class SessionsController < Devise::SessionsController
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
- user.invalidate_otp_backup_code!(user_params[:otp_attempt])
+ user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end
def log_audit_event(user, options = {})
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index c4508ccc3b9..6e29f1e8a65 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -86,7 +86,7 @@ class UsersController < ApplicationController
end
def exists
- render json: { exists: Namespace.where(path: params[:username].downcase).any? }
+ render json: { exists: !!Namespace.find_by_path_or_name(params[:username]) }
end
private
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 6297b2db369..1576fc80a6b 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -7,7 +7,7 @@
# current_user - which user use
# params:
# scope: 'created-by-me' or 'assigned-to-me' or 'all'
-# state: 'open' or 'closed' or 'all'
+# state: 'opened' or 'closed' or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
@@ -15,15 +15,14 @@
# search: string
# label_name: string
# sort: string
+# non_archived: boolean
#
-require_relative 'projects_finder'
-
class IssuableFinder
NONE = '0'
attr_accessor :current_user, :params
- def initialize(current_user, params)
+ def initialize(current_user, params = {})
@current_user = current_user
@params = params
end
@@ -40,9 +39,48 @@ class IssuableFinder
items = by_author(items)
items = by_label(items)
items = by_due_date(items)
+ items = by_non_archived(items)
sort(items)
end
+ def find(*params)
+ execute.find(*params)
+ end
+
+ def find_by(*params)
+ execute.find_by(*params)
+ end
+
+ # We often get counts for each state by running a query per state, and
+ # counting those results. This is typically slower than running one query
+ # (even if that query is slower than any of the individual state queries) and
+ # grouping and counting within that query.
+ #
+ def count_by_state
+ count_params = params.merge(state: nil, sort: nil)
+ labels_count = label_names.any? ? label_names.count : 1
+ finder = self.class.new(current_user, count_params)
+ counts = Hash.new(0)
+
+ # Searching by label includes a GROUP BY in the query, but ours will be last
+ # because it is added last. Searching by multiple labels also includes a row
+ # per issuable, so we have to count those in Ruby - which is bad, but still
+ # better than performing multiple queries.
+ #
+ finder.execute.reorder(nil).group(:state).count.each do |key, value|
+ counts[Array(key).last.to_sym] += value / labels_count
+ end
+
+ counts[:all] = counts.values.sum
+ counts[:opened] += counts[:reopened]
+
+ counts
+ end
+
+ def find_by!(*params)
+ execute.find_by!(*params)
+ end
+
def group
return @group if defined?(@group)
@@ -127,31 +165,53 @@ class IssuableFinder
end
end
- def assignee?
- params[:assignee_id].present?
+ def assignee_id?
+ params[:assignee_id].present? && params[:assignee_id] != NONE
+ end
+
+ def assignee_username?
+ params[:assignee_username].present? && params[:assignee_username] != NONE
+ end
+
+ def no_assignee?
+ # Assignee_id takes precedence over assignee_username
+ params[:assignee_id] == NONE || params[:assignee_username] == NONE
end
def assignee
return @assignee if defined?(@assignee)
@assignee =
- if assignee? && params[:assignee_id] != NONE
- User.find(params[:assignee_id])
+ if assignee_id?
+ User.find_by(id: params[:assignee_id])
+ elsif assignee_username?
+ User.find_by(username: params[:assignee_username])
else
nil
end
end
- def author?
- params[:author_id].present?
+ def author_id?
+ params[:author_id].present? && params[:author_id] != NONE
+ end
+
+ def author_username?
+ params[:author_username].present? && params[:author_username] != NONE
+ end
+
+ def no_author?
+ # author_id takes precedence over author_username
+ params[:author_id] == NONE || params[:author_username] == NONE
end
def author
return @author if defined?(@author)
@author =
- if author? && params[:author_id] != NONE
- User.find(params[:author_id])
+ if author_id?
+ User.find_by(id: params[:author_id])
+ elsif author_username?
+ User.find_by(username: params[:author_username])
else
nil
end
@@ -175,10 +235,13 @@ class IssuableFinder
end
def by_state(items)
- params[:state] ||= 'all'
-
- if items.respond_to?(params[:state])
- items.public_send(params[:state])
+ case params[:state].to_s
+ when 'closed'
+ items.closed
+ when 'merged'
+ items.respond_to?(:merged) ? items.merged : items.closed
+ when 'opened'
+ items.opened
else
items
end
@@ -222,16 +285,24 @@ class IssuableFinder
end
def by_assignee(items)
- if assignee?
- items = items.where(assignee_id: assignee.try(:id))
+ if assignee
+ items = items.where(assignee_id: assignee.id)
+ elsif no_assignee?
+ items = items.where(assignee_id: nil)
+ elsif assignee_id? || assignee_username? # assignee not found
+ items = items.none
end
items
end
def by_author(items)
- if author?
- items = items.where(author_id: author.try(:id))
+ if author
+ items = items.where(author_id: author.id)
+ elsif no_author?
+ items = items.where(author_id: nil)
+ elsif author_id? || author_username? # author not found
+ items = items.none
end
items
@@ -321,6 +392,10 @@ class IssuableFinder
end
end
+ def by_non_archived(items)
+ params[:non_archived].present? ? items.non_archived : items
+ end
+
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index be00a219205..707eddd4d29 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder
private
def init_collection
- Issue.visible_to_user(current_user)
+ IssuesFinder.not_restricted_by_confidentiality(current_user)
end
def iid_pattern
@iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
end
+
+ def self.not_restricted_by_confidentiality(user)
+ return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
+
+ return Issue.all if user.admin?
+
+ Issue.where('
+ issues.confidential IS NULL
+ OR issues.confidential IS FALSE
+ OR (issues.confidential = TRUE
+ AND (issues.author_id = :user_id
+ OR issues.assignee_id = :user_id
+ OR issues.project_id IN(:project_ids)))',
+ user_id: user.id,
+ project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
+ end
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 865f093f04a..fa0e2a5e3d8 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -6,7 +6,7 @@ class LabelsFinder < UnionFinder
def execute(skip_authorization: false)
@skip_authorization = skip_authorization
- items = find_union(label_ids, Label)
+ items = find_union(label_ids, Label) || Label.none
items = with_title(items)
sort(items)
end
@@ -18,9 +18,11 @@ class LabelsFinder < UnionFinder
def label_ids
label_ids = []
- if project
- label_ids << project.group.labels if project.group.present?
- label_ids << project.labels
+ if project?
+ if project
+ label_ids << project.group.labels if project.group.present?
+ label_ids << project.labels
+ end
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
@@ -40,16 +42,16 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
- def group_id
- params[:group_id].presence
+ def group?
+ params[:group_id].present?
end
- def project_id
- params[:project_id].presence
+ def project?
+ params[:project_id].present?
end
- def projects_ids
- params[:project_ids]
+ def projects?
+ params[:project_ids].present?
end
def title
@@ -59,8 +61,9 @@ class LabelsFinder < UnionFinder
def project
return @project if defined?(@project)
- if project_id
- @project = find_project
+ if project?
+ @project = Project.find(params[:project_id])
+ @project = nil unless authorized_to_read_labels?(@project)
else
@project = nil
end
@@ -68,26 +71,20 @@ class LabelsFinder < UnionFinder
@project
end
- def find_project
- if skip_authorization
- Project.find_by(id: project_id)
- else
- available_projects.find_by(id: project_id)
- end
- end
-
def projects
return @projects if defined?(@projects)
- @projects = skip_authorization ? Project.all : available_projects
- @projects = @projects.in_namespace(group_id) if group_id
- @projects = @projects.where(id: projects_ids) if projects_ids
+ @projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user)
+ @projects = @projects.in_namespace(params[:group_id]) if group?
+ @projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil)
@projects
end
- def available_projects
- @available_projects ||= ProjectsFinder.new.execute(current_user)
+ def authorized_to_read_labels?(project)
+ return true if skip_authorization
+
+ Ability.allowed?(current_user, :read_label, project)
end
end
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
new file mode 100644
index 00000000000..702944404f5
--- /dev/null
+++ b/app/finders/members_finder.rb
@@ -0,0 +1,13 @@
+class MembersFinder < Projects::ApplicationController
+ def initialize(project_members, project_group)
+ @project_members = project_members
+ @project_group = project_group
+ end
+
+ def execute(current_user)
+ non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
+ group_members = @project_group.group_members.where.not(user_id: non_null_user_ids)
+ group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group)
+ group_members
+ end
+end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 3b254e7d9d5..8b82255445e 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -14,6 +14,7 @@
# search: string
# label_name: string
# sort: string
+# non_archived: boolean
#
class MergeRequestsFinder < IssuableFinder
def klass
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 0b7832e6583..4bd8c83081a 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -1,27 +1,102 @@
class NotesFinder
FETCH_OVERLAP = 5.seconds
- def execute(project, current_user, params)
- target_type = params[:target_type]
- target_id = params[:target_id]
- # Default to 0 to remain compatible with old clients
- last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i)
-
- notes =
- case target_type
- when "commit"
- project.notes.for_commit_id(target_id).non_diff_notes
- when "issue"
- project.issues.visible_to_user(current_user).find(target_id).notes.inc_author
- when "merge_request"
- project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
- when "snippet", "project_snippet"
- project.snippets.find(target_id).notes
+ # Used to filter Notes
+ # When used with target_type and target_id this returns notes specifically for the controller
+ #
+ # Arguments:
+ # current_user - which user check authorizations with
+ # project - which project to look for notes on
+ # params:
+ # target_type: string
+ # target_id: integer
+ # last_fetched_at: time
+ # search: string
+ #
+ def initialize(project, current_user, params = {})
+ @project = project
+ @current_user = current_user
+ @params = params
+ init_collection
+ end
+
+ def execute
+ @notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at]
+ @notes
+ end
+
+ private
+
+ def init_collection
+ if @params[:target_id]
+ @notes = on_target(@params[:target_type], @params[:target_id])
+ else
+ @notes = notes_of_any_type
+ end
+ end
+
+ def notes_of_any_type
+ types = %w(commit issue merge_request snippet)
+ note_relations = types.map { |t| notes_for_type(t) }
+ note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search]
+ UnionFinder.new.find_union(note_relations, Note)
+ end
+
+ def noteables_for_type(noteable_type)
+ case noteable_type
+ when "issue"
+ IssuesFinder.new(@current_user, project_id: @project.id).execute
+ when "merge_request"
+ MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
+ when "snippet", "project_snippet"
+ SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
+ else
+ raise 'invalid target_type'
+ end
+ end
+
+ def notes_for_type(noteable_type)
+ if noteable_type == "commit"
+ if Ability.allowed?(@current_user, :download_code, @project)
+ @project.notes.where(noteable_type: 'Commit')
+ else
+ Note.none
+ end
+ else
+ finder = noteables_for_type(noteable_type)
+ @project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil))
+ end
+ end
+
+ def on_target(target_type, target_id)
+ if target_type == "commit"
+ notes_for_type('commit').for_commit_id(target_id)
+ else
+ target = noteables_for_type(target_type).find(target_id)
+
+ if target.respond_to?(:related_notes)
+ target.related_notes
else
- raise 'invalid target_type'
+ target.notes
end
+ end
+ end
+
+ # Searches for notes matching the given query.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ def search(query, notes_relation = @notes)
+ pattern = "%#{query}%"
+ notes_relation.where(Note.arel_table[:note].matches(pattern))
+ end
+
+ # Notes changed since last fetch
+ # Uses overlapping intervals to avoid worrying about race conditions
+ def since_fetch_at(fetch_time)
+ # Default to 0 to remain compatible with old clients
+ last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
- # Use overlapping intervals to avoid worrying about race conditions
- notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
+ @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
end
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 00ff1611039..da6e6e87a6f 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,14 +1,17 @@
class SnippetsFinder
def execute(current_user, params = {})
filter = params[:filter]
+ user = params.fetch(:user, current_user)
case filter
when :all then
snippets(current_user).fresh
+ when :public then
+ Snippet.are_public.fresh
when :by_user then
- by_user(current_user, params[:user], params[:scope])
+ by_user(current_user, user, params[:scope])
when :by_project
- by_project(current_user, params[:project])
+ by_project(current_user, params[:project], params[:scope])
end
end
@@ -29,35 +32,35 @@ class SnippetsFinder
def by_user(current_user, user, scope)
snippets = user.snippets.fresh
- return snippets.are_public unless current_user
-
- if user == current_user
- case scope
- when 'are_internal' then
- snippets.are_internal
- when 'are_private' then
- snippets.are_private
- when 'are_public' then
- snippets.are_public
- else
- snippets
- end
+ if current_user
+ include_private = user == current_user
+ by_scope(snippets, scope, include_private)
else
- snippets.public_and_internal
+ snippets.are_public
end
end
- def by_project(current_user, project)
+ def by_project(current_user, project, scope)
snippets = project.snippets.fresh
if current_user
- if project.team.member?(current_user) || current_user.admin?
- snippets
- else
- snippets.public_and_internal
- end
+ include_private = project.team.member?(current_user) || current_user.admin?
+ by_scope(snippets, scope, include_private)
else
snippets.are_public
end
end
+
+ def by_scope(snippets, scope = nil, include_private = false)
+ case scope.to_s
+ when 'are_private'
+ include_private ? snippets.are_private : Snippet.none
+ when 'are_internal'
+ snippets.are_internal
+ when 'are_public'
+ snippets.are_public
+ else
+ include_private ? snippets : snippets.public_and_internal
+ end
+ end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index c816b616631..a112928c6de 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -244,7 +244,9 @@ module ApplicationHelper
scope: params[:scope],
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
+ assignee_username: params[:assignee_username],
author_id: params[:author_id],
+ author_username: params[:author_username],
search: params[:search],
label_name: params[:label_name]
}
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 45a567a1eba..60485160495 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -50,14 +50,14 @@ module ApplicationSettingsHelper
def restricted_level_checkboxes(help_block_id)
Gitlab::VisibilityLevel.options.map do |name, level|
checked = restricted_visibility_levels(true).include?(level)
- css_class = 'btn'
- css_class += ' active' if checked
- checkbox_name = 'application_setting[restricted_visibility_levels][]'
+ css_class = checked ? 'active' : ''
+ checkbox_name = "application_setting[restricted_visibility_levels][]"
- label_tag(checkbox_name, class: css_class) do
+ label_tag(name, class: css_class) do
check_box_tag(checkbox_name, level, checked,
autocomplete: 'off',
- 'aria-describedby' => help_block_id) + name
+ 'aria-describedby' => help_block_id,
+ id: name) + visibility_level_icon(level) + name
end
end
end
@@ -67,14 +67,14 @@ module ApplicationSettingsHelper
def import_sources_checkboxes(help_block_id)
Gitlab::ImportSources.options.map do |name, source|
checked = current_application_settings.import_sources.include?(source)
- css_class = 'btn'
- css_class += ' active' if checked
+ css_class = checked ? 'active' : ''
checkbox_name = 'application_setting[import_sources][]'
- label_tag(checkbox_name, class: css_class) do
+ label_tag(name, class: css_class) do
check_box_tag(checkbox_name, source, checked,
autocomplete: 'off',
- 'aria-describedby' => help_block_id) + name
+ 'aria-describedby' => help_block_id,
+ id: name.tr(' ', '_')) + name
end
end
end
@@ -100,4 +100,8 @@ module ApplicationSettingsHelper
options_for_select(options, @application_setting.repository_storages)
end
+
+ def sidekiq_queue_options_for_select
+ options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
+ end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index cd4d778e508..1ee6c1d3afa 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,9 +1,9 @@
module AuthHelper
- PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
- Gitlab.config.ldap.enabled
+ Gitlab::LDAP::Config.enabled?
end
def omniauth_enabled?
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 07ff6fb9488..c3508443d8a 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -188,7 +188,11 @@ module BlobHelper
end
def gitlab_ci_ymls
- @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
+ @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context])
+ end
+
+ def dockerfile_names
+ @dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names
end
def blob_editor_paths
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index fde297c588e..9fc69e12266 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -12,7 +12,7 @@ module BuildsHelper
build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
build_status: @build.status,
build_stage: @build.stage,
- state1: @build.trace_with_state[:state]
+ log_state: @build.trace_with_state[:state].to_s
}
end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index dee3c78df47..4c7c16d694c 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -16,7 +16,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
- title = data[:title] || 'Copy to Clipboard'
+ title = data[:title] || 'Copy to clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 895c3d728ad..94f3b480178 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -1,20 +1,15 @@
module CiStatusHelper
def ci_status_path(pipeline)
project = pipeline.project
- builds_namespace_project_commit_path(project.namespace, project, pipeline.sha)
+ namespace_project_pipeline_path(project.namespace, project, pipeline)
end
- def ci_status_with_icon(status, target = nil)
- content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
- klass = "ci-status ci-#{status}"
- if target
- link_to content, target, class: klass
- else
- content_tag :span, content, class: klass
+ # Is used by Commit and Merge Request Widget
+ def ci_label_for_status(status)
+ if detailed_status?(status)
+ return status.label
end
- end
- def ci_label_for_status(status)
case status
when 'success'
'passed'
@@ -31,6 +26,10 @@ module CiStatusHelper
end
def ci_icon_for_status(status)
+ if detailed_status?(status)
+ return custom_icon(status.icon)
+ end
+
icon_name =
case status
when 'success'
@@ -94,4 +93,10 @@ module CiStatusHelper
class: klass, title: title, data: data
end
end
+
+ def detailed_status?(status)
+ status.respond_to?(:text) &&
+ status.respond_to?(:label) &&
+ status.respond_to?(:icon)
+ end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index ed402b698fb..e9461b9f859 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -128,50 +128,11 @@ module CommitsHelper
end
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
- return unless current_user
-
- tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip
-
- if can_collaborate_with_project?
- btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
- link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
- elsif can?(current_user, :fork_project, @project)
- continue_params = {
- to: continue_to_path,
- notice: edit_in_new_fork_notice + ' Try to revert this commit again.',
- notice_now: edit_in_new_fork_notice_now
- }
- fork_path = namespace_project_forks_path(@project.namespace, @project,
- namespace_key: current_user.namespace.id,
- continue: continue_params)
-
- btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
-
- link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
- end
+ commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
- return unless current_user
-
- tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
-
- if can_collaborate_with_project?
- btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
- link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
- elsif can?(current_user, :fork_project, @project)
- continue_params = {
- to: continue_to_path,
- notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.',
- notice_now: edit_in_new_fork_notice_now
- }
- fork_path = namespace_project_forks_path(@project.namespace, @project,
- namespace_key: current_user.namespace.id,
- continue: continue_params)
-
- btn_class = "btn btn-grouped btn-close" unless btn_class.nil?
- link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
- end
+ commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
protected
@@ -211,6 +172,28 @@ module CommitsHelper
end
end
+ def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true)
+ return unless current_user
+
+ tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
+ btn_class = "btn btn-#{btn_class}" unless btn_class.nil?
+
+ if can_collaborate_with_project?
+ link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
+ elsif can?(current_user, :fork_project, @project)
+ continue_params = {
+ to: continue_to_path,
+ notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.",
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_forks_path(@project.namespace, @project,
+ namespace_key: current_user.namespace.id,
+ continue: continue_params)
+
+ link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
+ end
+ end
+
def view_file_btn(commit_sha, diff_new_path, project)
link_to(
namespace_project_blob_path(project.namespace, project,
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 0725c3f4c56..aed1d7c839f 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -51,11 +51,12 @@ module DiffHelper
html.html_safe
end
- def diff_line_content(line, line_type = nil)
+ def diff_line_content(line)
if line.blank?
- " &nbsp;".html_safe
+ "&nbsp;".html_safe
else
- line[0] = ' ' if %w[new old].include?(line_type)
+ # We can't use `sub` because the HTML-safeness of `line` will not survive.
+ line[0] = '' if line.start_with?('+', '-', ' ')
line
end
end
@@ -164,4 +165,10 @@ module DiffHelper
link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class]
end
+
+ def render_overflow_warning?(diff_files)
+ diffs = @merge_request_diff.presence || diff_files
+
+ diffs.overflow?
+ end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index cbab1fd5967..81e0b6bb5ae 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -43,7 +43,7 @@ module DropdownsHelper
default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
- output << icon('caret-down')
+ output << icon('chevron-down')
output.html_safe
end
end
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
new file mode 100644
index 00000000000..ff8550439d0
--- /dev/null
+++ b/app/helpers/environment_helper.rb
@@ -0,0 +1,31 @@
+module EnvironmentHelper
+ def environment_for_build(project, build)
+ return unless build.environment
+
+ project.environments.find_by(name: build.expanded_environment_name)
+ end
+
+ def environment_link_for_build(project, build)
+ environment = environment_for_build(project, build)
+ if environment
+ link_to environment.name, namespace_project_environment_path(project.namespace, project, environment)
+ else
+ content_tag :span, build.expanded_environment_name
+ end
+ end
+
+ def deployment_link(deployment, text: nil)
+ return unless deployment
+
+ link_label = text ? text : "##{deployment.iid}"
+
+ link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ end
+
+ def last_deployment_link_for_environment_build(project, build)
+ environment = environment_for_build(project, build)
+ return unless environment
+
+ deployment_link(environment.last_deployment)
+ end
+end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
new file mode 100644
index 00000000000..515e802e01e
--- /dev/null
+++ b/app/helpers/environments_helper.rb
@@ -0,0 +1,7 @@
+module EnvironmentsHelper
+ def environments_list_data
+ {
+ endpoint: namespace_project_environments_path(@project.namespace, @project, format: :json)
+ }
+ end
+end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 00e64076408..362046c0270 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -45,6 +45,12 @@ module EventsHelper
@project.feature_available?(feature_key, current_user)
end
+ def comments_visible?
+ event_filter_visible(:repository) ||
+ event_filter_visible(:merge_requests) ||
+ event_filter_visible(:issues)
+ end
+
def event_preposition(event)
if event.push? || event.commented? || event.target
"at"
@@ -86,7 +92,7 @@ module EventsHelper
elsif event.merge_request?
namespace_project_merge_request_url(event.project.namespace,
event.project, event.merge_request)
- elsif event.note? && event.commit_note?
+ elsif event.commit_note?
namespace_project_commit_url(event.project.namespace, event.project,
event.note_target)
elsif event.note?
@@ -127,7 +133,7 @@ module EventsHelper
end
def event_note_target_path(event)
- if event.note? && event.commit_note?
+ if event.commit_note?
namespace_project_commit_path(event.project.namespace,
event.project,
event.note_target,
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 6a43be2cf3e..1182939f656 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -7,12 +7,12 @@ module FormHelper
content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
content_tag(:h4, headline) <<
- content_tag(:ul) do
- model.errors.full_messages.
- map { |msg| content_tag(:li, msg) }.
- join.
- html_safe
- end
+ content_tag(:ul) do
+ model.errors.full_messages.
+ map { |msg| content_tag(:li, msg) }.
+ join.
+ html_safe
+ end
end
end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 0772d848289..6d365ea9251 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -110,6 +110,28 @@ module GitlabMarkdownHelper
end
end
+ # Returns the text necessary to reference `entity` across projects
+ #
+ # project - Project to reference
+ # entity - Object that responds to `to_reference`
+ #
+ # Examples:
+ #
+ # cross_project_reference(project, project.issues.first)
+ # # => 'namespace1/project1#123'
+ #
+ # cross_project_reference(project, project.merge_requests.first)
+ # # => 'namespace1/project1!345'
+ #
+ # Returns a String
+ def cross_project_reference(project, entity)
+ if entity.respond_to?(:to_reference)
+ entity.to_reference(project, full: true)
+ else
+ ''
+ end
+ end
+
private
# Return +text+, truncated to +max_chars+ characters, excluding any HTML
@@ -158,28 +180,6 @@ module GitlabMarkdownHelper
end
end
- # Returns the text necessary to reference `entity` across projects
- #
- # project - Project to reference
- # entity - Object that responds to `to_reference`
- #
- # Examples:
- #
- # cross_project_reference(project, project.issues.first)
- # # => 'namespace1/project1#123'
- #
- # cross_project_reference(project, project.merge_requests.first)
- # # => 'namespace1/project1!345'
- #
- # Returns a String
- def cross_project_reference(project, entity)
- if entity.respond_to?(:to_reference)
- "#{project.to_reference}#{entity.to_reference}"
- else
- ''
- end
- end
-
def markdown_toolbar_button(options = {})
data = options[:data].merge({ container: "body" })
content_tag :button,
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index bccf64d1aac..2159e4ce21a 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -82,6 +82,10 @@ module GitlabRoutingHelper
namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args)
end
+ def pipeline_path(pipeline, *args)
+ namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
+ end
+
def milestone_path(entity, *args)
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end
@@ -155,6 +159,11 @@ module GitlabRoutingHelper
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
+ # Snippets
+ def personal_snippet_url(snippet, *args)
+ snippet_url(snippet)
+ end
+
# Groups
## Members
@@ -197,4 +206,13 @@ module GitlabRoutingHelper
file_namespace_project_build_artifacts_path(*args)
end
end
+
+ # Settings
+ def project_settings_integrations_path(project, *args)
+ namespace_project_settings_integrations_path(project.namespace, project, *args)
+ end
+
+ def project_settings_members_path(project, *args)
+ namespace_project_settings_members_path(project.namespace, project, *args)
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index ab880ed6de0..77dc9e7d538 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -5,22 +5,25 @@ module GroupsHelper
def group_icon(group)
if group.is_a?(String)
- group = Group.find_by(path: group)
+ group = Group.find_by_full_path(group)
end
- if group && group.avatar.present?
- group.avatar.url
- else
- image_path('no_group_avatar.png')
- end
+ group.try(:avatar_url) || image_path('no_group_avatar.png')
end
def group_title(group, name = nil, url = nil)
- full_title = link_to(simple_sanitize(group.name), group_path(group))
+ full_title = ''
+
+ group.parents.each do |parent|
+ full_title += link_to(simple_sanitize(parent.name), group_path(parent))
+ full_title += ' / '.html_safe
+ end
+
+ full_title += link_to(simple_sanitize(group.name), group_path(group))
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
content_tag :span do
- full_title
+ full_title.html_safe
end
end
@@ -48,4 +51,8 @@ module GroupsHelper
"#{status.humanize} #{projects_lfs_status(group)}"
end
end
+
+ def group_issues(group)
+ IssuesFinder.new(current_user, group_id: group.id).execute
+ end
end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index 021d2b14718..a0642a1894b 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -4,8 +4,10 @@ module ImportHelper
"#{namespace}/#{name}"
end
- def github_project_link(path_with_namespace)
- link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank'
+ def provider_project_link(provider, path_with_namespace)
+ url = __send__("#{provider}_project_url", path_with_namespace)
+
+ link_to path_with_namespace, url, target: '_blank'
end
private
@@ -20,4 +22,8 @@ module ImportHelper
provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' }
@github_url = provider.fetch('url', 'https://github.com') if provider
end
+
+ def gitea_project_url(path_with_namespace)
+ "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}"
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 8127c3f3ee3..e5bb8b93e76 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -30,9 +30,13 @@ module IssuablesHelper
end
end
- def can_add_template?(issuable)
- names = issuable_templates(issuable)
- names.empty? && can?(current_user, :push_code, @project) && !@project.private?
+ def serialize_issuable(issuable)
+ case issuable
+ when Issue
+ IssueSerializer.new.represent(issuable).to_json
+ when MergeRequest
+ MergeRequestSerializer.new.represent(issuable).to_json
+ end
end
def template_dropdown_tag(issuable, &block)
@@ -101,8 +105,8 @@ module IssuablesHelper
if issuable.tasks?
output << "&ensp;".html_safe
- output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs")
- output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-sm hidden-md hidden-lg")
+ output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm")
+ output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg")
end
output
@@ -141,8 +145,33 @@ module IssuablesHelper
html.html_safe
end
+ def cached_assigned_issuables_count(assignee, issuable_type, state)
+ cache_key = hexdigest(['assigned_issuables_count', assignee.id, issuable_type, state].join('-'))
+ Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
+ assigned_issuables_count(assignee, issuable_type, state)
+ end
+ end
+
+ def issuable_filter_params
+ [
+ :search,
+ :author_id,
+ :assignee_id,
+ :milestone_title,
+ :label_name
+ ]
+ end
+
+ def issuable_filter_present?
+ issuable_filter_params.any? { |k| params.key?(k) }
+ end
+
private
+ def assigned_issuables_count(assignee, issuable_type, state)
+ assignee.public_send("assigned_#{issuable_type}").public_send(state).count
+ end
+
def sidebar_gutter_collapsed?
cookies[:collapsed_gutter] == 'true'
end
@@ -159,15 +188,10 @@ module IssuablesHelper
end
end
- def issuable_filters_present
- params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name]
- end
-
def issuables_count_for_state(issuable_type, state)
- issuables_finder = public_send("#{issuable_type}_finder")
- issuables_finder.params[:state] = state
-
- issuables_finder.execute.page(1).total_count
+ @counts ||= {}
+ @counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state
+ @counts[issuable_type][state]
end
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
@@ -177,6 +201,7 @@ module IssuablesHelper
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
+ opts.delete_if { |_, value| value.blank? }
hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 1644c346dd8..a2d21b67a77 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -58,12 +58,14 @@ module IssuesHelper
end
def status_box_class(item)
- if item.respond_to?(:expired?) && item.expired?
+ if item.try(:expired?)
'status-box-expired'
- elsif item.respond_to?(:merged?) && item.merged?
+ elsif item.try(:merged?)
'status-box-merged'
elsif item.closed?
'status-box-closed'
+ elsif item.try(:upcoming?)
+ 'status-box-upcoming'
else
'status-box-open'
end
@@ -126,8 +128,10 @@ module IssuesHelper
names.to_sentence
end
- def award_active_class(awards, current_user)
- if current_user && awards.find { |a| a.user_id == current_user.id }
+ def award_state_class(awards, current_user)
+ if !current_user
+ "disabled"
+ elsif current_user && awards.find { |a| a.user_id == current_user.id }
"active"
else
""
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 221a84b042f..e5b1e6e8bc7 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -68,14 +68,6 @@ module LabelsHelper
end
end
- def toggle_subscription_data(label)
- return unless label.is_a?(ProjectLabel)
-
- {
- url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label)
- }
- end
-
def render_colored_label(label, label_suffix = '', tooltip: true)
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
@@ -90,12 +82,6 @@ module LabelsHelper
span.html_safe
end
- def render_colored_cross_project_label(label, source_project = nil, tooltip: true)
- label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace
- label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
- render_colored_label(label, label_suffix, tooltip: tooltip)
- end
-
def suggested_colors
[
'#0033CC',
@@ -148,20 +134,24 @@ module LabelsHelper
end
end
- def label_subscription_status(label)
- case label
- when GroupLabel then 'Subscribing to group labels is currently not supported.'
- when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
- end
+ def label_subscription_status(label, project)
+ return 'project-level' if label.subscribed?(current_user, project)
+ return 'group-level' if label.subscribed?(current_user)
+
+ 'unsubscribed'
end
- def label_subscription_toggle_button_text(label)
- case label
- when GroupLabel then 'Subscribing to group labels is currently not supported.'
- when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ def group_label_unsubscribe_path(label, project)
+ case label_subscription_status(label, project)
+ when 'project-level' then toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)
+ when 'group-level' then toggle_subscription_group_label_path(label.group, label)
end
end
+ def label_subscription_toggle_button_text(label, project)
+ label.subscribed?(current_user, project) ? 'Unsubscribe' : 'Subscribe'
+ end
+
def label_deletion_confirm_text(label)
case label
when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
@@ -170,6 +160,5 @@ module LabelsHelper
end
# Required for Banzai::Filter::LabelReferenceFilter
- module_function :render_colored_label, :render_colored_cross_project_label,
- :text_color_for_bg, :escape_once
+ module_function :render_colored_label, :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb
new file mode 100644
index 00000000000..49ac12db832
--- /dev/null
+++ b/app/helpers/mattermost_helper.rb
@@ -0,0 +1,9 @@
+module MattermostHelper
+ def mattermost_teams_options(teams)
+ teams_options = teams.map do |id, options|
+ [options['display_name'] || options['name'], id]
+ end
+
+ teams_options.compact.unshift(['Select team...', '0'])
+ end
+end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index 877c77050be..41d471cc92f 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -36,4 +36,12 @@ module MembersHelper
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end
+
+ def filter_group_project_member_path(options = {})
+ options = params.slice(:search, :sort).merge(options)
+
+ path = request.path
+ path << "?#{options.to_param}"
+ path
+ end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index a6659ea2fd1..8c2c4e8833b 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -19,6 +19,14 @@ module MergeRequestsHelper
}
end
+ def mr_widget_refresh_url(mr)
+ if mr && mr.source_project
+ merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr)
+ else
+ ''
+ end
+ end
+
def mr_css_classes(mr)
classes = "merge-request"
classes << " closed" if mr.closed?
@@ -59,6 +67,10 @@ module MergeRequestsHelper
@mr_closes_issues ||= @merge_request.closes_issues
end
+ def mr_issues_mentioned_but_not_closing
+ @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
+ end
+
def mr_change_branches_path(merge_request)
new_namespace_project_merge_request_path(
@project.namespace, @project,
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 83a2a4ad3ec..729928ce1dd 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -86,6 +86,30 @@ module MilestonesHelper
days = milestone.remaining_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} remaining"
+ elsif milestone.upcoming?
+ content_tag(:strong, 'Upcoming')
+ elsif milestone.start_date && milestone.start_date.past?
+ days = milestone.elapsed_days
+ content = content_tag(:strong, days)
+ content << " #{'day'.pluralize(days)} elapsed"
+ end
+ end
+
+ def milestone_date_range(milestone)
+ if milestone.start_date && milestone.due_date
+ "#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}"
+ elsif milestone.due_date
+ if milestone.due_date.past?
+ "expired on #{milestone.due_date.to_s(:medium)}"
+ else
+ "expires on #{milestone.due_date.to_s(:medium)}"
+ end
+ elsif milestone.start_date
+ if milestone.start_date.past?
+ "started on #{milestone.start_date.to_s(:medium)}"
+ else
+ "starts on #{milestone.start_date.to_s(:medium)}"
+ end
end
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index df87fac132d..e21178c7377 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -7,12 +7,12 @@ module NavHelper
def page_gutter_class
if current_path?('merge_requests#show') ||
- current_path?('merge_requests#diffs') ||
- current_path?('merge_requests#commits') ||
- current_path?('merge_requests#builds') ||
- current_path?('merge_requests#conflicts') ||
- current_path?('merge_requests#pipelines') ||
- current_path?('issues#show')
+ current_path?('merge_requests#diffs') ||
+ current_path?('merge_requests#commits') ||
+ current_path?('merge_requests#builds') ||
+ current_path?('merge_requests#conflicts') ||
+ current_path?('merge_requests#pipelines') ||
+ current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
else
@@ -20,6 +20,11 @@ module NavHelper
end
elsif current_path?('builds#show')
"page-gutter build-sidebar right-sidebar-expanded"
+ elsif current_path?('wikis#show') ||
+ current_path?('wikis#edit') ||
+ current_path?('wikis#history') ||
+ current_path?('wikis#git_access')
+ "page-gutter wiki-sidebar right-sidebar-expanded"
end
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index a46f2c6e17d..6e68aad4cb7 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -50,7 +50,7 @@ module PreferencesHelper
end
def default_project_view
- return 'readme' unless current_user
+ return anonymous_project_view unless current_user
user_view = current_user.project_view
@@ -66,4 +66,8 @@ module PreferencesHelper
"customize_workflow"
end
end
+
+ def anonymous_project_view
+ @project.empty_repo? || !can?(current_user, :download_code, @project) ? 'activity' : 'readme'
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 42c00ec3cd5..eb98204285d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -49,10 +49,10 @@ module ProjectsHelper
end
end
- def project_title(project, name = nil, url = nil)
+ def project_title(project)
namespace_link =
if project.group
- link_to(simple_sanitize(project.group.name), group_path(project.group))
+ group_title(project.group)
else
owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner))
@@ -61,15 +61,12 @@ module ProjectsHelper
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
if current_user
- project_link << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do
+ project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do
icon("chevron-down")
end
end
- full_title = "#{namespace_link} / #{project_link}".html_safe
- full_title << ' &middot; '.html_safe << link_to(simple_sanitize(name), url) if name
-
- full_title
+ "#{namespace_link} / #{project_link}".html_safe
end
def remove_project_message(project)
@@ -93,10 +90,12 @@ module ProjectsHelper
end
def project_for_deploy_key(deploy_key)
- if deploy_key.projects.include?(@project)
+ if deploy_key.has_access_to?(@project)
@project
else
- deploy_key.projects.find { |project| can?(current_user, :read_project, project) }
+ deploy_key.projects.find do |project|
+ can?(current_user, :read_project, project)
+ end
end
end
@@ -174,48 +173,27 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
- if can?(current_user, :read_pipeline, project)
- nav_tabs << :pipelines
- end
-
- if can?(current_user, :read_build, project)
- nav_tabs << :builds
- end
-
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
nav_tabs << :container_registry
end
- if can?(current_user, :read_environment, project)
- nav_tabs << :environments
- end
-
- if can?(current_user, :admin_project, project)
- nav_tabs << :settings
- end
-
- if can?(current_user, :read_project_member, project)
- nav_tabs << :team
- end
-
- if can?(current_user, :read_issue, project)
- nav_tabs << :issues
- end
-
- if can?(current_user, :read_wiki, project)
- nav_tabs << :wiki
- end
-
- if can?(current_user, :read_project_snippet, project)
- nav_tabs << :snippets
- end
-
- if can?(current_user, :read_label, project)
- nav_tabs << :labels
- end
+ tab_ability_map = {
+ environments: :read_environment,
+ milestones: :read_milestone,
+ pipelines: :read_pipeline,
+ snippets: :read_project_snippet,
+ settings: :admin_project,
+ builds: :read_build,
+ labels: :read_label,
+ issues: :read_issue,
+ team: :read_project_member,
+ wiki: :read_wiki
+ }
- if can?(current_user, :read_milestone, project)
- nav_tabs << :milestones
+ tab_ability_map.each do |tab, ability|
+ if can?(current_user, ability, project)
+ nav_tabs << tab
+ end
end
nav_tabs.flatten
@@ -249,11 +227,6 @@ module ProjectsHelper
end
end
- def repository_size(project = @project)
- size_in_bytes = project.repository_size * 1.megabyte
- number_to_human_size(size_in_bytes, delimiter: ',', precision: 2)
- end
-
def default_url_to_repo(project = @project)
case default_clone_protocol
when 'ssh'
@@ -283,13 +256,15 @@ module ProjectsHelper
end
end
- def add_special_file_path(project, file_name:, commit_message: nil)
+ def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil)
namespace_project_new_blob_path(
project.namespace,
project,
project.default_branch || 'master',
file_name: file_name,
- commit_message: commit_message || "Add #{file_name.downcase}"
+ commit_message: commit_message || "Add #{file_name.downcase}",
+ target_branch: target_branch,
+ context: context
)
end
@@ -394,39 +369,11 @@ module ProjectsHelper
end
end
- def new_readme_path
- ref = @repository.root_ref if @repository
- ref ||= 'master'
-
- namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md')
- end
-
- def new_license_path
- ref = @repository.root_ref if @repository
- ref ||= 'master'
-
- namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE')
- end
-
def readme_cache_key
sha = @project.commit.try(:sha) || 'nil'
[@project.path_with_namespace, sha, "readme"].join('-')
end
- def round_commit_count(project)
- count = project.commit_count
-
- if count > 10000
- '10000+'
- elsif count > 5000
- '5000+'
- elsif count > 1000
- '1000+'
- else
- count
- end
- end
-
def current_ref
@ref || @repository.try(:root_ref)
end
@@ -458,4 +405,19 @@ module ProjectsHelper
def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end
+
+ def project_issues(project)
+ IssuesFinder.new(current_user, project_id: project.id).execute
+ end
+
+ def visibility_select_options(project, selected_level)
+ levels_options_array = Gitlab::VisibilityLevel.values.map do |level|
+ [
+ visibility_level_label(level),
+ { data: { description: visibility_level_description(level, project) } },
+ level
+ ]
+ end
+ options_for_select(levels_options_array, selected_level)
+ end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index aba3a3f9c5d..6654f6997ce 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -31,34 +31,7 @@ module SearchHelper
end
def parse_search_result(result)
- ref = nil
- filename = nil
- basename = nil
- startline = 0
-
- result.each_line.each_with_index do |line, index|
- if line =~ /^.*:.*:\d+:/
- ref, filename, startline = line.split(':')
- startline = startline.to_i - index
- extname = Regexp.escape(File.extname(filename))
- basename = filename.sub(/#{extname}$/, '')
- break
- end
- end
-
- data = ""
-
- result.each_line do |line|
- data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
- end
-
- OpenStruct.new(
- filename: filename,
- basename: basename,
- ref: ref,
- startline: startline,
- data: data
- )
+ Gitlab::ProjectSearchResults.parse_search_result(result)
end
private
@@ -66,7 +39,7 @@ module SearchHelper
# Autocomplete results for various settings pages
def default_autocomplete
[
- { category: "Settings", label: "Profile settings", url: profile_path },
+ { category: "Settings", label: "User settings", url: profile_path },
{ category: "Settings", label: "SSH Keys", url: profile_keys_path },
{ category: "Settings", label: "Dashboard", url: root_path },
{ category: "Settings", label: "Admin Section", url: admin_root_path },
@@ -102,7 +75,7 @@ module SearchHelper
{ category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
{ category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
{ category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
- { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Members", url: namespace_project_settings_members_path(@project.namespace, @project) },
{ category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
]
else
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 3d4abf76419..715e5893a2c 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -1,22 +1,24 @@
module ServicesHelper
def service_event_description(event)
case event
- when "push"
+ when "push", "push_events"
"Event will be triggered by a push to the repository"
- when "tag_push"
+ when "tag_push", "tag_push_events"
"Event will be triggered when a new tag is pushed to the repository"
- when "note"
+ when "note", "note_events"
"Event will be triggered when someone adds a comment"
- when "issue"
+ when "issue", "issue_events"
"Event will be triggered when an issue is created/updated/closed"
- when "confidential_issue"
+ when "confidential_issue", "confidential_issue_events"
"Event will be triggered when a confidential issue is created/updated/closed"
- when "merge_request"
+ when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged"
- when "build"
+ when "build", "build_events"
"Event will be triggered when a build status changes"
- when "wiki_page"
+ when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated"
+ when "commit", "commit_events"
+ "Event will be triggered when a commit is created/updated"
end
end
@@ -24,4 +26,6 @@ module ServicesHelper
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
end
+
+ extend self
end
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
index 56749d80bd3..b5017080cfb 100644
--- a/app/helpers/sidekiq_helper.rb
+++ b/app/helpers/sidekiq_helper.rb
@@ -5,15 +5,11 @@ module SidekiqHelper
(?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+
(?<start>.+)\s+
- (?<command>sidekiq.*\])\s*
+ (?<command>sidekiq.*\])
\z/x
def parse_sidekiq_ps(line)
- match = line.match(SIDEKIQ_PS_REGEXP)
- if match
- match[1..6]
- else
- %w[? ? ? ? ? ?]
- end
+ match = line.strip.match(SIDEKIQ_PS_REGEXP)
+ match ? match[1..6] : Array.new(6, '?')
end
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 7e33a562077..8c02b4061ca 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -8,6 +8,17 @@ module SnippetsHelper
end
end
+ # Return the path of a snippets index for a user or for a project
+ #
+ # @returns String, path to snippet index
+ def subject_snippets_path(subject = nil, opts = nil)
+ if subject.is_a?(Project)
+ namespace_project_snippets_path(subject.namespace, subject, opts)
+ else # assume subject === User
+ dashboard_snippets_path(opts)
+ end
+ end
+
# Get an array of line numbers surrounding a matching
# line, bounded by min/max.
#
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 8b138a8e69f..ff787fb4131 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -11,6 +11,7 @@ module SortingHelper
sort_value_due_date_soon => sort_title_due_date_soon,
sort_value_due_date_later => sort_title_due_date_later,
sort_value_largest_repo => sort_title_largest_repo,
+ sort_value_largest_group => sort_title_largest_group,
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin,
sort_value_downvotes => sort_title_downvotes,
@@ -25,7 +26,7 @@ module SortingHelper
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
- sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_oldest_created => sort_title_oldest_created
}
if current_controller?('admin/projects')
@@ -35,6 +36,19 @@ module SortingHelper
options
end
+ def member_sort_options_hash
+ {
+ sort_value_access_level_asc => sort_title_access_level_asc,
+ sort_value_access_level_desc => sort_title_access_level_desc,
+ sort_value_last_joined => sort_title_last_joined,
+ sort_value_oldest_joined => sort_title_oldest_joined,
+ sort_value_name => sort_title_name_asc,
+ sort_value_name_desc => sort_title_name_desc,
+ sort_value_recently_signin => sort_title_recently_signin,
+ sort_value_oldest_signin => sort_title_oldest_signin
+ }
+ end
+
def sort_title_priority
'Priority'
end
@@ -79,6 +93,10 @@ module SortingHelper
'Largest repository'
end
+ def sort_title_largest_group
+ 'Largest group'
+ end
+
def sort_title_recently_signin
'Recent sign in'
end
@@ -95,6 +113,50 @@ module SortingHelper
'Most popular'
end
+ def sort_title_last_joined
+ 'Last joined'
+ end
+
+ def sort_title_oldest_joined
+ 'Oldest joined'
+ end
+
+ def sort_title_access_level_asc
+ 'Access level, ascending'
+ end
+
+ def sort_title_access_level_desc
+ 'Access level, descending'
+ end
+
+ def sort_title_name_asc
+ 'Name, ascending'
+ end
+
+ def sort_title_name_desc
+ 'Name, descending'
+ end
+
+ def sort_value_last_joined
+ 'last_joined'
+ end
+
+ def sort_value_oldest_joined
+ 'oldest_joined'
+ end
+
+ def sort_value_access_level_asc
+ 'access_level_asc'
+ end
+
+ def sort_value_access_level_desc
+ 'access_level_desc'
+ end
+
+ def sort_value_name_desc
+ 'name_desc'
+ end
+
def sort_value_priority
'priority'
end
@@ -136,7 +198,11 @@ module SortingHelper
end
def sort_value_largest_repo
- 'repository_size_desc'
+ 'storage_size_desc'
+ end
+
+ def sort_value_largest_group
+ 'storage_size_desc'
end
def sort_value_recently_signin
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
new file mode 100644
index 00000000000..e19c67a37ca
--- /dev/null
+++ b/app/helpers/storage_helper.rb
@@ -0,0 +1,7 @@
+module StorageHelper
+ def storage_counter(size_in_bytes)
+ precision = size_in_bytes < 1.megabyte ? 0 : 1
+
+ number_to_human_size(size_in_bytes, delimiter: ',', precision: precision, significant: false)
+ end
+end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 563ddd2a511..547f6258909 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -106,9 +106,9 @@ module TabHelper
def branches_tab_class
if current_controller?(:protected_branches) ||
- current_controller?(:branches) ||
- current_page?(namespace_project_repository_path(@project.namespace,
- @project))
+ current_controller?(:branches) ||
+ current_page?(namespace_project_repository_path(@project.namespace,
+ @project))
'active'
end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 09c69786791..c568cca9e5e 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -11,9 +11,10 @@ module TodosHelper
case todo.action
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
- when Todo::BUILD_FAILED then 'The build failed for your'
+ when Todo::BUILD_FAILED then 'The build failed for'
when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
+ when Todo::UNMERGEABLE then 'Could not merge'
end
end
@@ -35,7 +36,7 @@ module TodosHelper
else
path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
- path.unshift(:builds) if todo.build_failed?
+ path.unshift(:pipelines) if todo.build_failed?
polymorphic_path(path, anchor: anchor)
end
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
index 8cad994d10f..b0135ea2e95 100644
--- a/app/helpers/triggers_helper.rb
+++ b/app/helpers/triggers_helper.rb
@@ -1,5 +1,13 @@
module TriggersHelper
- def builds_trigger_url(project_id)
- "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+ def builds_trigger_url(project_id, ref: nil)
+ if ref.nil?
+ "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+ else
+ "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds"
+ end
+ end
+
+ def service_trigger_url(service)
+ "#{Settings.gitlab.url}/api/v3/projects/#{service.project_id}/services/#{service.to_param}/trigger"
end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 96116e916dd..0d20c9092c4 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -4,6 +4,7 @@ module Emails
setup_note_mail(note_id, recipient_id)
@commit = @note.noteable
+ @discussion = @note.to_discussion if @note.diff_note?
@target_url = namespace_project_commit_url(*note_target_url_options)
mail_answer_thread(@commit,
@@ -24,6 +25,7 @@ module Emails
setup_note_mail(note_id, recipient_id)
@merge_request = @note.noteable
+ @discussion = @note.to_discussion if @note.diff_note?
@target_url = namespace_project_merge_request_url(*note_target_url_options)
mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0bc1c19e9cd..0cd3456b4de 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -107,15 +107,11 @@ class Notify < BaseMailer
def mail_thread(model, headers = {})
add_project_headers
+ add_unsubscription_headers_and_links
+
headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
- if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
- headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
-
- @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
- end
-
if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
@@ -171,4 +167,16 @@ class Notify < BaseMailer
headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.path_with_namespace
end
+
+ def add_unsubscription_headers_and_links
+ return unless !@labels_url && @sent_notification && @sent_notification.unsubscribable?
+
+ list_unsubscribe_methods = [unsubscribe_sent_notification_url(@sent_notification, force: true)]
+ if Gitlab::IncomingEmail.enabled? && Gitlab::IncomingEmail.supports_wildcard?
+ list_unsubscribe_methods << "mailto:#{Gitlab::IncomingEmail.unsubscribe_address(reply_key)}"
+ end
+
+ headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',')
+ @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index bb60cc8736c..8fab77cda0a 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -19,6 +19,7 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array
serialize :domain_blacklist, Array
serialize :repository_storages
+ serialize :sidekiq_throttling_queues, Array
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
@@ -67,6 +68,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :koding_enabled
+ validates :plantuml_url,
+ presence: true,
+ if: :plantuml_enabled
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -85,6 +90,15 @@ class ApplicationSetting < ActiveRecord::Base
presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
if: :domain_blacklist_enabled?
+ validates :sidekiq_throttling_factor,
+ numericality: { greater_than: 0, less_than: 1 },
+ presence: { message: 'Throttling factor cannot be empty if Sidekiq Throttling is enabled.' },
+ if: :sidekiq_throttling_enabled?
+
+ validates :sidekiq_throttling_queues,
+ presence: { message: 'Queues to throttle cannot be empty if Sidekiq Throttling is enabled.' },
+ if: :sidekiq_throttling_enabled?
+
validates :housekeeping_incremental_repack_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -174,12 +188,15 @@ class ApplicationSetting < ActiveRecord::Base
akismet_enabled: false,
koding_enabled: false,
koding_url: nil,
+ plantuml_enabled: false,
+ plantuml_url: nil,
repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
container_registry_token_expire_delay: 5,
repository_storages: ['default'],
user_default_external: false,
+ sidekiq_throttling_enabled: false,
housekeeping_enabled: true,
housekeeping_bitmaps_enabled: true,
housekeeping_incremental_repack_period: 10,
@@ -192,6 +209,10 @@ class ApplicationSetting < ActiveRecord::Base
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end
+ def sidekiq_throttling_column_exists?
+ ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled)
+ end
+
def domain_whitelist_raw
self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
end
@@ -245,6 +266,12 @@ class ApplicationSetting < ActiveRecord::Base
ensure_health_check_access_token!
end
+ def sidekiq_throttling_enabled?
+ return false unless sidekiq_throttling_column_exists?
+
+ sidekiq_throttling_enabled
+ end
+
private
def check_repository_storages
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
new file mode 100644
index 00000000000..f321db75eeb
--- /dev/null
+++ b/app/models/chat_name.rb
@@ -0,0 +1,12 @@
+class ChatName < ActiveRecord::Base
+ belongs_to :service
+ belongs_to :user
+
+ validates :user, presence: true
+ validates :service, presence: true
+ validates :team_id, presence: true
+ validates :chat_id, presence: true
+
+ validates :user_id, uniqueness: { scope: [:service_id] }
+ validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index bf5f92f8462..5fe8ddf69d7 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -2,13 +2,24 @@ module Ci
class Build < CommitStatus
include TokenAuthenticatable
include AfterCommitQueue
+ include Presentable
belongs_to :runner
belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
+ has_many :deployments, as: :deployable
+
+ # The "environment" field for builds is a String, and is the unexpanded name
+ def persisted_environment
+ @persisted_environment ||= Environment.find_by(
+ name: expanded_environment_name,
+ project_id: gl_project_id
+ )
+ end
+
serialize :options
- serialize :yaml_variables
+ serialize :yaml_variables, Gitlab::Serialize::Ci::Variables
validates :coverage, numericality: true, allow_blank: true
validates_presence_of :ref
@@ -33,6 +44,8 @@ module Ci
before_destroy { project }
after_create :execute_hooks
+ after_save :update_project_statistics, if: :artifacts_size_changed?
+ after_destroy :update_project_statistics
class << self
def first_pending
@@ -68,13 +81,23 @@ module Ci
environment: build.environment,
status_event: 'enqueue'
)
- MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
+
+ MergeRequests::AddTodoWhenBuildFailsService
+ .new(build.project, nil)
+ .close(new_build)
+
build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build
end
end
state_machine :status do
+ after_transition any => [:pending] do |build|
+ build.run_after_commit do
+ BuildQueueWorker.perform_async(id)
+ end
+ end
+
after_transition pending: :running do |build|
build.run_after_commit do
BuildHooksWorker.perform_async(id)
@@ -94,6 +117,12 @@ module Ci
end
end
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Build::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
+
def manual?
self.when == 'manual'
end
@@ -117,14 +146,47 @@ module Ci
end
end
+ def cancelable?
+ active?
+ end
+
def retryable?
- project.builds_enabled? && commands.present? && complete?
+ project.builds_enabled? && commands.present? &&
+ (success? || failed? || canceled?)
end
def retried?
!self.pipeline.statuses.latest.include?(self)
end
+ def expanded_environment_name
+ ExpandVariables.expand(environment, simple_variables) if environment
+ end
+
+ def has_environment?
+ environment.present?
+ end
+
+ def starts_environment?
+ has_environment? && self.environment_action == 'start'
+ end
+
+ def stops_environment?
+ has_environment? && self.environment_action == 'stop'
+ end
+
+ def environment_action
+ self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options
+ end
+
+ def outdated_deployment?
+ success? && !last_deployment.try(:last?)
+ end
+
+ def last_deployment
+ deployments.last
+ end
+
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
@@ -150,12 +212,25 @@ module Ci
project.build_timeout
end
- def variables
+ # A slugified version of the build ref, suitable for inclusion in URLs and
+ # domain names. Rules:
+ #
+ # * Lowercased
+ # * Anything not matching [a-z0-9-] is replaced with a -
+ # * Maximum length is 63 bytes
+ def ref_slug
+ slugified = ref.to_s.downcase
+ slugified.gsub(/[^a-z0-9]/, '-')[0..62]
+ end
+
+ # Variables whose value does not depend on other variables
+ def simple_variables
variables = predefined_variables
variables += project.predefined_variables
variables += pipeline.predefined_variables
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
+ variables += project.deployment_variables if has_environment?
variables += yaml_variables
variables += user_variables
variables += project.secret_variables
@@ -163,13 +238,20 @@ module Ci
variables
end
+ # All variables, including those dependent on other variables
+ def variables
+ variables = simple_variables
+ variables += persisted_environment.predefined_variables if persisted_environment.present?
+ variables
+ end
+
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
.where(source_branch: ref, source_project_id: pipeline.gl_project_id)
.reorder(iid: :asc)
merge_requests.find do |merge_request|
- merge_request.commits.any? { |ci| ci.id == pipeline.sha }
+ merge_request.commits_sha.include?(pipeline.sha)
end
end
@@ -271,6 +353,7 @@ module Ci
def append_trace(trace_part, offset)
recreate_trace_dir
+ touch if needs_touch?
trace_part = hide_secrets(trace_part)
@@ -280,6 +363,10 @@ module Ci
end
end
+ def needs_touch?
+ Time.now - updated_at > 15.minutes.to_i
+ end
+
def trace_file_path
if has_old_trace_file?
old_path_to_trace
@@ -427,6 +514,10 @@ module Ci
end
end
+ def has_expiring_artifacts?
+ artifacts_expire_at.present?
+ end
+
def keep_artifacts!
self.update(artifacts_expire_at: nil)
end
@@ -448,6 +539,10 @@ module Ci
]
end
+ def credentials
+ Gitlab::Ci::Build::Credentials::Factory.new(self).create!
+ end
+
private
def update_artifacts_size
@@ -475,6 +570,7 @@ module Ci
{ key: 'CI_BUILD_REF', value: sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: ref, public: true },
+ { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_BUILD_NAME', value: name, public: true },
{ key: 'CI_BUILD_STAGE', value: stage, public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
@@ -501,5 +597,9 @@ module Ci
Ci::MaskSecret.mask!(trace, token)
trace
end
+
+ def update_project_statistics
+ ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
+ end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 3fee6c18770..fab8497ec7d 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -21,8 +21,6 @@ module Ci
after_create :keep_around_commits, unless: :importing?
- delegate :stages, to: :statuses
-
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
@@ -90,25 +88,66 @@ module Ci
end
# ref can't be HEAD or SHA, can only be branch/tag name
+ scope :latest, ->(ref = nil) do
+ max_id = unscope(:select)
+ .select("max(#{quoted_table_name}.id)")
+ .group(:ref, :sha)
+
+ relation = ref ? where(ref: ref) : self
+ relation.where(id: max_id)
+ end
+
+ def self.latest_status(ref = nil)
+ latest(ref).status
+ end
+
def self.latest_successful_for(ref)
- where(ref: ref).order(id: :desc).success.first
+ success.latest(ref).order(id: :desc).first
end
def self.truncate_sha(sha)
sha[0...8]
end
- def self.stages
- # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
- CommitStatus.where(pipeline: pluck(:id)).stages
- end
-
def self.total_duration
where.not(duration: nil).sum(:duration)
end
- def stages_with_latest_statuses
- statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
+ def stage(name)
+ stage = Ci::Stage.new(self, name: name)
+ stage unless stage.statuses_count.zero?
+ end
+
+ def stages_count
+ statuses.select(:stage).distinct.count
+ end
+
+ def stages_name
+ statuses.order(:stage_idx).distinct.
+ pluck(:stage, :stage_idx).map(&:first)
+ end
+
+ def stages
+ # TODO, this needs refactoring, see gitlab-ce#26481.
+
+ stages_query = statuses
+ .group('stage').select(:stage).order('max(stage_idx)')
+
+ status_sql = statuses.latest.where('stage=sg.stage').status_sql
+
+ warnings_sql = statuses.latest.select('COUNT(*) > 0')
+ .where('stage=sg.stage').failed_but_allowed.to_sql
+
+ stages_with_statuses = CommitStatus.from(stages_query, :sg)
+ .pluck('sg.stage', status_sql, "(#{warnings_sql})")
+
+ stages_with_statuses.map do |stage|
+ Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
+ end
+ end
+
+ def artifacts
+ builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end
def project_id
@@ -157,27 +196,35 @@ module Ci
end
def manual_actions
- builds.latest.manual_actions
+ builds.latest.manual_actions.includes(project: [:namespace])
+ end
+
+ def stuck?
+ builds.pending.any?(&:stuck?)
end
def retryable?
- builds.latest.any? do |build|
- (build.failed? || build.canceled?) && build.retryable?
- end
+ builds.latest.failed_or_canceled.any?(&:retryable?)
end
def cancelable?
- builds.running_or_pending.any?
+ statuses.cancelable.any?
end
def cancel_running
- builds.running_or_pending.each(&:cancel)
+ Gitlab::OptimisticLocking.retry_lock(
+ statuses.cancelable) do |cancelable|
+ cancelable.each(&:cancel)
+ end
end
def retry_failed(user)
- builds.latest.failed.select(&:retryable?).each do |build|
- Ci::Build.retry(build, user)
- end
+ Gitlab::OptimisticLocking.retry_lock(
+ builds.latest.failed_or_canceled) do |failed_or_canceled|
+ failed_or_canceled.select(&:retryable?).each do |build|
+ Ci::Build.retry(build, user)
+ end
+ end
end
def mark_as_processable_after_stage(stage_idx)
@@ -245,6 +292,10 @@ module Ci
end
end
+ def has_yaml_errors?
+ yaml_errors.present?
+ end
+
def environments
builds.where.not(environment: nil).success.pluck(:environment).uniq
end
@@ -313,7 +364,13 @@ module Ci
def merge_requests
@merge_requests ||= project.merge_requests
.where(source_branch: self.ref)
- .select { |merge_request| merge_request.pipeline.try(:id) == self.id }
+ .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
+ end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Pipeline::Factory
+ .new(self, current_user)
+ .fabricate!
end
private
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 123930273e0..ed1843ba005 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,6 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
+ RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
@@ -21,6 +22,8 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) }
+ after_save :tick_runner_queue, if: :form_editable_changed?
+
scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
@@ -122,8 +125,38 @@ module Ci
]
end
+ def tick_runner_queue
+ SecureRandom.hex.tap do |new_update|
+ Gitlab::Redis.with do |redis|
+ redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME)
+ end
+ end
+ end
+
+ def ensure_runner_queue_value
+ Gitlab::Redis.with do |redis|
+ value = SecureRandom.hex
+ redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true)
+ redis.get(runner_queue_key)
+ end
+ end
+
+ def is_runner_queue_value_latest?(value)
+ ensure_runner_queue_value == value if value.present?
+ end
+
private
+ def runner_queue_key
+ "runner:build_queue:#{self.token}"
+ end
+
+ def form_editable_changed?
+ FORM_EDITABLE.any? do |editable|
+ public_send("#{editable}_changed?")
+ end
+ end
+
def tag_constraints
unless has_tags? || run_untagged?
errors.add(:tags_list,
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
new file mode 100644
index 00000000000..ca74c91b062
--- /dev/null
+++ b/app/models/ci/stage.rb
@@ -0,0 +1,56 @@
+module Ci
+ # Currently this is artificial object, constructed dynamically
+ # We should migrate this object to actual database record in the future
+ class Stage
+ include StaticModel
+
+ attr_reader :pipeline, :name
+
+ delegate :project, to: :pipeline
+
+ def initialize(pipeline, name:, status: nil, warnings: nil)
+ @pipeline = pipeline
+ @name = name
+ @status = status
+ @warnings = warnings
+ end
+
+ def to_param
+ name
+ end
+
+ def statuses_count
+ @statuses_count ||= statuses.count
+ end
+
+ def status
+ @status ||= statuses.latest.status
+ end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Stage::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
+
+ def statuses
+ @statuses ||= pipeline.statuses.where(stage: name)
+ end
+
+ def builds
+ @builds ||= pipeline.builds.where(stage: name)
+ end
+
+ def success?
+ status.to_s == 'success'
+ end
+
+ def has_warnings?
+ if @warnings.nil?
+ statuses.latest.failed_but_allowed.any?
+ else
+ @warnings
+ end
+ end
+ end
+end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 94d9e2b3208..2c8698d8b5d 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -4,10 +4,10 @@ module Ci
belongs_to :project, foreign_key: :gl_project_id
- validates_uniqueness_of :key, scope: :gl_project_id
validates :key,
presence: true,
- length: { within: 0..255 },
+ uniqueness: { scope: :gl_project_id },
+ length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 9e7fde9503d..316bd2e512b 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -21,6 +21,9 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000
+ # The SHA can be between 7 and 40 hex characters.
+ COMMIT_SHA_PATTERN = '\h{7,40}'
+
class << self
def decorate(commits, project)
commits.map do |commit|
@@ -48,6 +51,14 @@ class Commit
max_lines: DIFF_HARD_LIMIT_LINES,
}
end
+
+ def from_hash(hash, project)
+ new(Gitlab::Git::Commit.new(hash), project)
+ end
+
+ def valid_hash?(key)
+ !!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
+ end
end
attr_accessor :raw
@@ -73,8 +84,6 @@ class Commit
# Pattern used to extract commit references from text
#
- # The SHA can be between 7 and 40 hex characters.
- #
# This pattern supports cross-project references.
def self.reference_pattern
@reference_pattern ||= %r{
@@ -84,23 +93,15 @@ class Commit
end
def self.link_reference_pattern
- @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/)
+ @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
end
- def to_reference(from_project = nil)
- if cross_project_reference?(from_project)
- project.to_reference + self.class.reference_prefix + self.id
- else
- self.id
- end
+ def to_reference(from_project = nil, full: false)
+ commit_reference(from_project, id, full: full)
end
def reference_link_text(from_project = nil)
- if cross_project_reference?(from_project)
- project.to_reference + self.class.reference_prefix + self.short_id
- else
- self.short_id
- end
+ commit_reference(from_project, short_id)
end
def diff_line_count
@@ -232,13 +233,9 @@ class Commit
def status(ref = nil)
@statuses ||= {}
- if @statuses.key?(ref)
- @statuses[ref]
- elsif ref
- @statuses[ref] = pipelines.where(ref: ref).status
- else
- @statuses[ref] = pipelines.status
- end
+ return @statuses[ref] if @statuses.key?(ref)
+
+ @statuses[ref] = pipelines.latest_status(ref)
end
def revert_branch_name
@@ -249,44 +246,47 @@ class Commit
project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
end
- def revert_description
- if merged_merge_request
- "This reverts merge request #{merged_merge_request.to_reference}"
+ def revert_description(user)
+ if merged_merge_request?(user)
+ "This reverts merge request #{merged_merge_request(user).to_reference}"
else
"This reverts commit #{sha}"
end
end
- def revert_message
- %Q{Revert "#{title.strip}"\n\n#{revert_description}}
+ def revert_message(user)
+ %Q{Revert "#{title.strip}"\n\n#{revert_description(user)}}
end
- def reverts_commit?(commit)
- description? && description.include?(commit.revert_description)
+ def reverts_commit?(commit, user)
+ description? && description.include?(commit.revert_description(user))
end
def merge_commit?
parents.size > 1
end
- def merged_merge_request
- return @merged_merge_request if defined?(@merged_merge_request)
+ def merged_merge_request(current_user)
+ # Memoize with per-user access check
+ @merged_merge_request_hash ||= Hash.new do |hash, user|
+ hash[user] = merged_merge_request_no_cache(user)
+ end
- @merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit?
+ @merged_merge_request_hash[current_user]
end
- def has_been_reverted?(current_user = nil, noteable = self)
+ def has_been_reverted?(current_user, noteable = self)
ext = all_references(current_user)
noteable.notes_with_associations.system.each do |note|
note.all_references(current_user, extractor: ext)
end
- ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) }
+ ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) }
end
- def change_type_title
- merged_merge_request ? 'merge request' : 'commit'
+ def change_type_title(user)
+ merged_merge_request?(user) ? 'merge request' : 'commit'
end
# Get the URI type of the given path
@@ -323,8 +323,32 @@ class Commit
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
end
+ def persisted?
+ true
+ end
+
+ def touch
+ # no-op but needs to be defined since #persisted? is defined
+ end
+
+ WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
+
+ def work_in_progress?
+ !!(title =~ WIP_REGEX)
+ end
+
private
+ def commit_reference(from_project, referable_commit_id, full: false)
+ reference = project.to_reference(from_project, full: full)
+
+ if reference.present?
+ "#{reference}#{self.class.reference_prefix}#{referable_commit_id}"
+ else
+ referable_commit_id
+ end
+ end
+
def find_author_by_any_email
User.find_by_any_email(author_email.downcase)
end
@@ -344,4 +368,12 @@ class Commit
changes
end
+
+ def merged_merge_request?(user)
+ !!merged_merge_request(user)
+ end
+
+ def merged_merge_request_no_cache(user)
+ MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
+ end
end
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index ac2477fd973..84e2e8a5dd5 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -89,22 +89,25 @@ class CommitRange
alias_method :id, :to_s
- def to_reference(from_project = nil)
- if cross_project_reference?(from_project)
- project.to_reference + self.class.reference_prefix + self.id
+ def to_reference(from_project = nil, full: false)
+ project_reference = project.to_reference(from_project, full: full)
+
+ if project_reference.present?
+ project_reference + self.class.reference_prefix + self.id
else
self.id
end
end
def reference_link_text(from_project = nil)
- reference = ref_from + notation + ref_to
+ project_reference = project.to_reference(from_project)
+ reference = ref_from + notation + ref_to
- if cross_project_reference?(from_project)
- reference = project.to_reference + self.class.reference_prefix + reference
+ if project_reference.present?
+ project_reference + self.class.reference_prefix + reference
+ else
+ reference
end
-
- reference
end
# Return a Hash of parameters for passing to a URL helper
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index d159fc6c5c7..9547c57b2ae 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -31,18 +31,13 @@ class CommitStatus < ActiveRecord::Base
end
scope :exclude_ignored, -> do
- quoted_when = connection.quote_column_name('when')
# We want to ignore failed_but_allowed jobs
where("allow_failure = ? OR status IN (?)",
- false, all_state_names - [:failed, :canceled]).
- # We want to ignore skipped manual jobs
- where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped').
- # We want to ignore skipped on_failure
- where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
+ false, all_state_names - [:failed, :canceled])
end
- scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
- scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
+ scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
+ scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
state_machine :status do
event :enqueue do
@@ -117,33 +112,35 @@ class CommitStatus < ActiveRecord::Base
name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
end
- def self.stages
- # We group by stage name, but order stages by theirs' index
- unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
- end
-
- def self.stages_status
- # We execute subquery for each stage to calculate a stage status
- statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql)
- statuses.inject({}) do |h, k|
- h[k.first] = k.last
- h
- end
- end
-
def failed_but_allowed?
allow_failure? && (failed? || canceled?)
end
+ def duration
+ calculate_duration
+ end
+
def playable?
false
end
- def duration
- calculate_duration
+ def stuck?
+ false
end
- def stuck?
+ def has_trace?
false
end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
+
+ def sortable_name
+ name.split(/(\d+)/).map do |v|
+ v =~ /\d+/ ? v.to_i : v
+ end
+ end
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index ef3e73a4072..431c0354969 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -1,10 +1,11 @@
module HasStatus
extend ActiveSupport::Concern
+ DEFAULT_STATUS = 'created'
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running]
- COMPLETED_STATUSES = %w[success failed canceled]
+ COMPLETED_STATUSES = %w[success failed canceled skipped]
ORDERED_STATUSES = %w[failed pending running canceled success skipped]
class_methods do
@@ -23,9 +24,10 @@ module HasStatus
canceled = scope.canceled.select('count(*)').to_sql
"(CASE
+ WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created'
- WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped'
+ WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
@@ -73,6 +75,11 @@ module HasStatus
scope :skipped, -> { where(status: 'skipped') }
scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) }
+ scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) }
+
+ scope :cancelable, -> do
+ where(status: [:running, :pending, :created])
+ end
end
def started?
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 664bb594aa9..3517969eabc 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -13,6 +13,7 @@ module Issuable
include StripAttribute
include Awardable
include Taskable
+ include TimeTrackable
included do
cache_markdown_field :title, pipeline: :single_line
@@ -41,7 +42,7 @@ module Issuable
has_one :metrics
validates :author, presence: true
- validates :title, presence: true, length: { within: 0..255 }
+ validates :title, presence: true, length: { maximum: 255 }
scope :authored, ->(user) { where(author_id: user) }
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
@@ -92,8 +93,9 @@ module Issuable
after_save :record_metrics
def update_assignee_cache_counts
- # make sure we flush the cache for both the old *and* new assignee
- User.find(assignee_id_was).update_cache_counts if assignee_id_was
+ # make sure we flush the cache for both the old *and* new assignees(if they exist)
+ previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
+ previous_assignee.update_cache_counts if previous_assignee
assignee.update_cache_counts if assignee
end
@@ -215,7 +217,7 @@ module Issuable
end
end
- def subscribed_without_subscriptions?(user)
+ def subscribed_without_subscriptions?(user, project)
participants(user).include?(user)
end
@@ -251,6 +253,17 @@ module Issuable
self.class.to_ability_name
end
+ # Convert this Issuable class name to a format usable by notifications.
+ #
+ # Examples:
+ #
+ # issuable.class # => MergeRequest
+ # issuable.human_class_name # => "merge request"
+
+ def human_class_name
+ @human_class_name ||= self.class.name.titleize.downcase
+ end
+
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index eb2ff0428f6..8ab0401d288 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -1,6 +1,6 @@
# == Mentionable concern
#
-# Contains functionality related to objects that can mention Users, Issues, MergeRequests, or Commits by
+# Contains functionality related to objects that can mention Users, Issues, MergeRequests, Commits or Snippets by
# GFM references.
#
# Used by Issue, Note, MergeRequest, and Commit.
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 7bcc78247ba..e9450dd0c26 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,17 +1,25 @@
module Milestoneish
- def closed_items_count(user = nil)
- issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
+ def closed_items_count(user)
+ memoize_per_user(user, :closed_items_count) do
+ (count_issues_by_state(user)['closed'] || 0) + merge_requests.closed_and_merged.size
+ end
end
- def total_items_count(user = nil)
- issues_visible_to_user(user).size + merge_requests.size
+ def total_items_count(user)
+ memoize_per_user(user, :total_items_count) do
+ total_issues_count(user) + merge_requests.size
+ end
end
- def complete?(user = nil)
+ def total_issues_count(user)
+ count_issues_by_state(user).values.sum
+ end
+
+ def complete?(user)
total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
end
- def percent_complete(user = nil)
+ def percent_complete(user)
((closed_items_count(user) * 100) / total_items_count(user)).abs
rescue ZeroDivisionError
0
@@ -23,7 +31,54 @@ module Milestoneish
(due_date - Date.today).to_i
end
- def issues_visible_to_user(user = nil)
- issues.visible_to_user(user)
+ def elapsed_days
+ return 0 if !start_date || start_date.future?
+
+ (Date.today - start_date).to_i
+ end
+
+ def issues_visible_to_user(user)
+ memoize_per_user(user, :issues_visible_to_user) do
+ IssuesFinder.new(user, issues_finder_params)
+ .execute.where(milestone_id: milestoneish_ids)
+ end
+ end
+
+ def upcoming?
+ start_date && start_date.future?
+ end
+
+ def expires_at
+ if due_date
+ if due_date.past?
+ "expired on #{due_date.to_s(:medium)}"
+ else
+ "expires on #{due_date.to_s(:medium)}"
+ end
+ end
+ end
+
+ def expired?
+ due_date && due_date.past?
+ end
+
+ private
+
+ def count_issues_by_state(user)
+ memoize_per_user(user, :count_issues_by_state) do
+ issues_visible_to_user(user).reorder(nil).group(:state).count
+ end
+ end
+
+ def memoize_per_user(user, method_name)
+ @memoized ||= {}
+ @memoized[method_name] ||= {}
+ @memoized[method_name][user.try!(:id)] ||= yield
+ end
+
+ # override in a class that includes this module to get a faster query
+ # from IssuesFinder
+ def issues_finder_params
+ {}
end
end
diff --git a/app/models/concerns/presentable.rb b/app/models/concerns/presentable.rb
new file mode 100644
index 00000000000..7b33b837004
--- /dev/null
+++ b/app/models/concerns/presentable.rb
@@ -0,0 +1,7 @@
+module Presentable
+ def present(**attributes)
+ Gitlab::View::Presenter::Factory
+ .new(self, attributes)
+ .fabricate!
+ end
+end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 6d88951c713..60734bc6660 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility
build_project_feature unless project_feature
access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
- project_feature.update_attribute(field, access_level)
+ project_feature.send(:write_attribute, field, access_level)
end
end
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 7fd0905ee81..9dd4d9c6f24 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -2,6 +2,9 @@ module ProtectedBranchAccess
extend ActiveSupport::Concern
included do
+ belongs_to :protected_branch
+ delegate :project, to: :protected_branch
+
scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
end
@@ -9,4 +12,10 @@ module ProtectedBranchAccess
def humanize
self.class.human_access_levels[self.access_level]
end
+
+ def check_access(user)
+ return true if user.is_admin?
+
+ project.team.max_member_access(user.id) >= access_level
+ end
end
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
new file mode 100644
index 00000000000..2589215ad19
--- /dev/null
+++ b/app/models/concerns/reactive_caching.rb
@@ -0,0 +1,118 @@
+# The ReactiveCaching concern is used to fetch some data in the background and
+# store it in the Rails cache, keeping it up-to-date for as long as it is being
+# requested. If the data hasn't been requested for +reactive_cache_lifetime+,
+# it stop being refreshed, and then be removed.
+#
+# Example of use:
+#
+# class Foo < ActiveRecord::Base
+# include ReactiveCaching
+#
+# self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
+#
+# after_save :clear_reactive_cache!
+#
+# def calculate_reactive_cache
+# # Expensive operation here. The return value of this method is cached
+# end
+#
+# def result
+# with_reactive_cache do |data|
+# # ...
+# end
+# end
+# end
+#
+# In this example, the first time `#result` is called, it will return `nil`.
+# However, it will enqueue a background worker to call `#calculate_reactive_cache`
+# and set an initial cache lifetime of ten minutes.
+#
+# Each time the background job completes, it stores the return value of
+# `#calculate_reactive_cache`. It is also re-enqueued to run again after
+# `reactive_cache_refresh_interval`, so keeping the stored value up to date.
+# Calculations are never run concurrently.
+#
+# Calling `#result` while a value is in the cache will call the block given to
+# `#with_reactive_cache`, yielding the cached value. It will also extend the
+# lifetime by `reactive_cache_lifetime`.
+#
+# Once the lifetime has expired, no more background jobs will be enqueued and
+# calling `#result` will again return `nil` - starting the process all over
+# again
+module ReactiveCaching
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :reactive_cache_lease_timeout
+
+ class_attribute :reactive_cache_key
+ class_attribute :reactive_cache_lifetime
+ class_attribute :reactive_cache_refresh_interval
+
+ # defaults
+ self.reactive_cache_lease_timeout = 2.minutes
+
+ self.reactive_cache_refresh_interval = 1.minute
+ self.reactive_cache_lifetime = 10.minutes
+
+ def calculate_reactive_cache(*args)
+ raise NotImplementedError
+ end
+
+ def with_reactive_cache(*args, &blk)
+ within_reactive_cache_lifetime(*args) do
+ data = Rails.cache.read(full_reactive_cache_key(*args))
+ yield data if data.present?
+ end
+ ensure
+ Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
+ ReactiveCachingWorker.perform_async(self.class, id, *args)
+ end
+
+ def clear_reactive_cache!(*args)
+ Rails.cache.delete(full_reactive_cache_key(*args))
+ end
+
+ def exclusively_update_reactive_cache!(*args)
+ locking_reactive_cache(*args) do
+ within_reactive_cache_lifetime(*args) do
+ enqueuing_update(*args) do
+ value = calculate_reactive_cache(*args)
+ Rails.cache.write(full_reactive_cache_key(*args), value)
+ end
+ end
+ end
+ end
+
+ private
+
+ def full_reactive_cache_key(*qualifiers)
+ prefix = self.class.reactive_cache_key
+ prefix = prefix.call(self) if prefix.respond_to?(:call)
+
+ ([prefix].flatten + qualifiers).join(':')
+ end
+
+ def alive_reactive_cache_key(*qualifiers)
+ full_reactive_cache_key(*(qualifiers + ['alive']))
+ end
+
+ def locking_reactive_cache(*args)
+ lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key(*args), timeout: reactive_cache_lease_timeout)
+ uuid = lease.try_obtain
+ yield if uuid
+ ensure
+ Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid)
+ end
+
+ def within_reactive_cache_lifetime(*args)
+ yield if Rails.cache.read(alive_reactive_cache_key(*args))
+ end
+
+ def enqueuing_update(*args)
+ yield
+ ensure
+ ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args)
+ end
+ end
+end
diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb
new file mode 100644
index 00000000000..e1f868a299b
--- /dev/null
+++ b/app/models/concerns/reactive_service.rb
@@ -0,0 +1,10 @@
+module ReactiveService
+ extend ActiveSupport::Concern
+
+ included do
+ include ReactiveCaching
+
+ # Default cache key: class name + project_id
+ self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+ end
+end
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index dee940a3f88..da803c7f481 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -17,7 +17,7 @@ module Referable
# Issue.last.to_reference(other_project) # => "cross-project#1"
#
# Returns a String
- def to_reference(_from_project = nil)
+ def to_reference(_from_project = nil, full:)
''
end
@@ -72,17 +72,4 @@ module Referable
}x
end
end
-
- private
-
- # Check if a reference is being done cross-project
- #
- # from_project - Refering Project object
- def cross_project_reference?(from_project)
- if self.is_a?(Project)
- self != from_project
- else
- from_project && self.project && self.project != from_project
- end
- end
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
new file mode 100644
index 00000000000..1108a64c59e
--- /dev/null
+++ b/app/models/concerns/routable.rb
@@ -0,0 +1,71 @@
+# Store object full path in separate table for easy lookup and uniq validation
+# Object must have path db field and respond to full_path and full_path_changed? methods.
+module Routable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :route, as: :source, autosave: true, dependent: :destroy
+
+ validates_associated :route
+ validates :route, presence: true
+
+ before_validation :update_route_path, if: :full_path_changed?
+ end
+
+ class_methods do
+ # Finds a single object by full path match in routes table.
+ #
+ # Usage:
+ #
+ # Klass.find_by_full_path('gitlab-org/gitlab-ce')
+ #
+ # Returns a single object, or nil.
+ def find_by_full_path(path)
+ # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
+ # any literal matches come first, for this we have to use "BINARY".
+ # Without this there's still no guarantee in what order MySQL will return
+ # rows.
+ binary = Gitlab::Database.mysql? ? 'BINARY' : ''
+
+ order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
+
+ where_full_path_in([path]).reorder(order_sql).take
+ end
+
+ # Builds a relation to find multiple objects by their full paths.
+ #
+ # Usage:
+ #
+ # Klass.where_full_path_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
+ #
+ # Returns an ActiveRecord::Relation.
+ def where_full_path_in(paths)
+ wheres = []
+ cast_lower = Gitlab::Database.postgresql?
+
+ paths.each do |path|
+ path = connection.quote(path)
+ where = "(routes.path = #{path})"
+
+ if cast_lower
+ where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))"
+ end
+
+ wheres << where
+ end
+
+ if wheres.empty?
+ none
+ else
+ joins(:route).where(wheres.join(' OR '))
+ end
+ end
+ end
+
+ private
+
+ def update_route_path
+ route || build_route(source: self)
+ route.path = full_path
+ end
+end
diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb
new file mode 100644
index 00000000000..50a1d7fc3e1
--- /dev/null
+++ b/app/models/concerns/select_for_project_authorization.rb
@@ -0,0 +1,9 @@
+module SelectForProjectAuthorization
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def select_for_project_authorization
+ select("members.user_id, projects.id AS project_id, members.access_level")
+ end
+ end
+end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 083257f1005..83daa9b1a64 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -12,39 +12,71 @@ module Subscribable
has_many :subscriptions, dependent: :destroy, as: :subscribable
end
- def subscribed?(user)
- if subscription = subscriptions.find_by_user_id(user.id)
+ def subscribed?(user, project = nil)
+ if subscription = subscriptions.find_by(user: user, project: project)
subscription.subscribed
else
- subscribed_without_subscriptions?(user)
+ subscribed_without_subscriptions?(user, project)
end
end
# Override this method to define custom logic to consider a subscribable as
# subscribed without an explicit subscription record.
- def subscribed_without_subscriptions?(user)
+ def subscribed_without_subscriptions?(user, project)
false
end
- def subscribers
- subscriptions.where(subscribed: true).map(&:user)
+ def subscribers(project)
+ subscriptions_available(project).
+ where(subscribed: true).
+ map(&:user)
end
- def toggle_subscription(user)
- subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: !subscribed?(user))
+ def toggle_subscription(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project).
+ update(subscribed: !subscribed?(user, project))
+ end
+
+ def subscribe(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project)
+ .update(subscribed: true)
+ end
+
+ def unsubscribe(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project)
+ .update(subscribed: false)
end
- def subscribe(user)
+ private
+
+ def unsubscribe_from_other_levels(user, project)
+ other_subscriptions = subscriptions.where(user: user)
+
+ other_subscriptions =
+ if project.blank?
+ other_subscriptions.where.not(project: nil)
+ else
+ other_subscriptions.where(project: nil)
+ end
+
+ other_subscriptions.update_all(subscribed: false)
+ end
+
+ def find_or_initialize_subscription(user, project)
subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: true)
+ find_or_initialize_by(user_id: user.id, project_id: project.try(:id))
end
- def unsubscribe(user)
+ def subscriptions_available(project)
+ t = Subscription.arel_table
+
subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: false)
+ where(t[:project_id].eq(nil).or(t[:project_id].eq(project.try(:id))))
end
end
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
new file mode 100644
index 00000000000..040e3a2884e
--- /dev/null
+++ b/app/models/concerns/time_trackable.rb
@@ -0,0 +1,72 @@
+# == TimeTrackable concern
+#
+# Contains functionality related to objects that support time tracking.
+#
+# Used by Issue and MergeRequest.
+#
+
+module TimeTrackable
+ extend ActiveSupport::Concern
+
+ included do
+ attr_reader :time_spent, :time_spent_user
+
+ alias_method :time_spent?, :time_spent
+
+ default_value_for :time_estimate, value: 0, allows_nil: false
+
+ validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
+ validate :check_negative_time_spent
+
+ has_many :timelogs, as: :trackable, dependent: :destroy
+ end
+
+ def spend_time(options)
+ @time_spent = options[:duration]
+ @time_spent_user = options[:user]
+ @original_total_time_spent = nil
+
+ return if @time_spent == 0
+
+ if @time_spent == :reset
+ reset_spent_time
+ else
+ add_or_subtract_spent_time
+ end
+ end
+ alias_method :spend_time=, :spend_time
+
+ def total_time_spent
+ timelogs.sum(:time_spent)
+ end
+
+ def human_total_time_spent
+ Gitlab::TimeTrackingFormatter.output(total_time_spent)
+ end
+
+ def human_time_estimate
+ Gitlab::TimeTrackingFormatter.output(time_estimate)
+ end
+
+ private
+
+ def reset_spent_time
+ timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user)
+ end
+
+ def add_or_subtract_spent_time
+ timelogs.new(time_spent: time_spent, user: @time_spent_user)
+ end
+
+ def check_negative_time_spent
+ return if time_spent.nil? || time_spent == :reset
+
+ # we need to cache the total time spent so multiple calls to #valid?
+ # doesn't give a false error
+ @original_total_time_spent ||= total_time_spent
+
+ if time_spent < 0 && (time_spent.abs > @original_total_time_spent)
+ errors.add(:time_spent, 'Time to subtract exceeds the total time spent')
+ end
+ end
+end
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 04d30f46210..1ca7f91dc03 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -39,6 +39,10 @@ module TokenAuthenticatable
current_token.blank? ? write_new_token(token_field) : current_token
end
+ define_method("set_#{token_field}") do |token|
+ write_attribute(token_field, token) if token
+ end
+
define_method("ensure_#{token_field}!") do
send("reset_#{token_field}!") if read_attribute(token_field).blank?
read_attribute(token_field)
diff --git a/app/models/concerns/valid_attribute.rb b/app/models/concerns/valid_attribute.rb
new file mode 100644
index 00000000000..8c35cea8d58
--- /dev/null
+++ b/app/models/concerns/valid_attribute.rb
@@ -0,0 +1,10 @@
+module ValidAttribute
+ extend ActiveSupport::Concern
+
+ # Checks whether an attribute has failed validation or not
+ #
+ # +attribute+ The symbolised name of the attribute i.e :name
+ def valid_attribute?(attribute)
+ self.errors.empty? || self.errors.messages[attribute].nil?
+ end
+end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index 8ed4a56b19b..d2e626c22e8 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -1,103 +1,38 @@
class CycleAnalytics
- include Gitlab::Database::Median
- include Gitlab::Database::DateTime
+ STAGES = %i[issue plan code test review staging production].freeze
- DEPLOYMENT_METRIC_STAGES = %i[production staging]
-
- def initialize(project, from:)
+ def initialize(project, options)
@project = project
- @from = from
+ @options = options
end
def summary
- @summary ||= Summary.new(@project, from: @from)
- end
-
- def issue
- calculate_metric(:issue,
- Issue.arel_table[:created_at],
- [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
- Issue::Metrics.arel_table[:first_added_to_board_at]])
- end
-
- def plan
- calculate_metric(:plan,
- [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
- Issue::Metrics.arel_table[:first_added_to_board_at]],
- Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
- end
-
- def code
- calculate_metric(:code,
- Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
- MergeRequest.arel_table[:created_at])
+ @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
+ from: @options[:from],
+ current_user: @options[:current_user]).data
end
- def test
- calculate_metric(:test,
- MergeRequest::Metrics.arel_table[:latest_build_started_at],
- MergeRequest::Metrics.arel_table[:latest_build_finished_at])
+ def stats
+ @stats ||= stats_per_stage
end
- def review
- calculate_metric(:review,
- MergeRequest.arel_table[:created_at],
- MergeRequest::Metrics.arel_table[:merged_at])
+ def no_stats?
+ stats.all? { |hash| hash[:value].nil? }
end
- def staging
- calculate_metric(:staging,
- MergeRequest::Metrics.arel_table[:merged_at],
- MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ def permissions(user:)
+ Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
end
- def production
- calculate_metric(:production,
- Issue.arel_table[:created_at],
- MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ def [](stage_name)
+ Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
end
private
- def calculate_metric(name, start_time_attrs, end_time_attrs)
- cte_table = Arel::Table.new("cte_table_for_#{name}")
-
- # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
- # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
- # We compute the (end_time - start_time) interval, and give it an alias based on the current
- # cycle analytics stage.
- interval_query = Arel::Nodes::As.new(
- cte_table,
- subtract_datetimes(base_query_for(name), end_time_attrs, start_time_attrs, name.to_s))
-
- median_datetime(cte_table, interval_query, name)
- end
-
- # Join table with a row for every <issue,merge_request> pair (where the merge request
- # closes the given issue) with issue and merge request metrics included. The metrics
- # are loaded with an inner join, so issues / merge requests without metrics are
- # automatically excluded.
- def base_query_for(name)
- arel_table = MergeRequestsClosingIssues.arel_table
-
- # Load issues
- query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])).
- join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])).
- where(Issue.arel_table[:project_id].eq(@project.id)).
- where(Issue.arel_table[:deleted_at].eq(nil)).
- where(Issue.arel_table[:created_at].gteq(@from))
-
- # Load merge_requests
- query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin).
- on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])).
- join(MergeRequest::Metrics.arel_table).
- on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
-
- if DEPLOYMENT_METRIC_STAGES.include?(name)
- # Limit to merge requests that have been deployed to production after `@from`
- query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
+ def stats_per_stage
+ STAGES.map do |stage_name|
+ self[stage_name].as_json
end
-
- query
end
end
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
index b46db449bf3..e69de29bb2d 100644
--- a/app/models/cycle_analytics/summary.rb
+++ b/app/models/cycle_analytics/summary.rb
@@ -1,42 +0,0 @@
-class CycleAnalytics
- class Summary
- def initialize(project, from:)
- @project = project
- @from = from
- end
-
- def new_issues
- @project.issues.created_after(@from).count
- end
-
- def commits
- ref = @project.default_branch.presence
- count_commits_for(ref)
- end
-
- def deploys
- @project.deployments.where("created_at > ?", @from).count
- end
-
- private
-
- # Don't use the `Gitlab::Git::Repository#log` method, because it enforces
- # a limit. Since we need a commit count, we _can't_ enforce a limit, so
- # the easiest way forward is to replicate the relevant portions of the
- # `log` function here.
- def count_commits_for(ref)
- return unless ref
-
- repository = @project.repository.raw_repository
- sha = @project.repository.commit(ref).sha
-
- cmd = %W(git --git-dir=#{repository.path} log)
- cmd << '--format=%H'
- cmd << "--after=#{@from.iso8601}"
- cmd << sha
-
- raw_output = IO.popen(cmd) { |io| io.read }
- raw_output.lines.count
- end
- end
-end
diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb
new file mode 100644
index 00000000000..646c1e5ce1a
--- /dev/null
+++ b/app/models/dashboard_milestone.rb
@@ -0,0 +1,5 @@
+class DashboardMilestone < GlobalMilestone
+ def issues_finder_params
+ { authorized_only: true }
+ end
+end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 2c525d4cd7a..053f2a11aa0 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -20,4 +20,18 @@ class DeployKey < Key
def destroyed_when_orphaned?
self.private?
end
+
+ def has_access_to?(project)
+ projects.include?(project)
+ end
+
+ def can_push_to?(project)
+ can_push? && has_access_to?(project)
+ end
+
+ private
+
+ # we don't want to notify the user for deploy keys
+ def notify_user
+ end
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index de06c13481a..bbe813db823 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -25,7 +25,12 @@ class Discussion
to: :last_resolved_note,
allow_nil: true
- delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true
+ delegate :blob,
+ :highlighted_diff_lines,
+ :diff_lines,
+
+ to: :diff_file,
+ allow_nil: true
def self.for_notes(notes)
notes.group_by(&:discussion_id).values.map { |notes| new(notes) }
@@ -83,6 +88,10 @@ class Discussion
@first_note ||= @notes.first
end
+ def first_note_to_resolve
+ @first_note_to_resolve ||= notes.detect(&:to_be_resolved?)
+ end
+
def last_note
@last_note ||= @notes.last
end
@@ -159,10 +168,11 @@ class Discussion
end
# Returns an array of at most 16 highlighted lines above a diff note
- def truncated_diff_lines
+ def truncated_diff_lines(highlight: true)
+ lines = highlight ? highlighted_diff_lines : diff_lines
prev_lines = []
- highlighted_diff_lines.each do |line|
+ lines.each do |line|
if line.meta?
prev_lines.clear
else
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 5c662bbab87..8bca47373f8 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -1,25 +1,38 @@
class Environment < ActiveRecord::Base
+ # Used to generate random suffixes for the slug
+ NUMBERS = '0'..'9'
+ SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+
belongs_to :project, required: true, validate: true
has_many :deployments
before_validation :nullify_external_url
+ before_validation :generate_slug, if: ->(env) { env.slug.blank? }
+
before_save :set_environment_type
validates :name,
presence: true,
uniqueness: { scope: :project_id },
- length: { within: 0..255 },
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
+ validates :slug,
+ presence: true,
+ uniqueness: { scope: :project_id },
+ length: { maximum: 24 },
+ format: { with: Gitlab::Regex.environment_slug_regex,
+ message: Gitlab::Regex.environment_slug_regex_message }
+
validates :external_url,
uniqueness: { scope: :project_id },
length: { maximum: 255 },
allow_nil: true,
addressable_url: true
- delegate :stop_action, to: :last_deployment, allow_nil: true
+ delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
@@ -37,6 +50,17 @@ class Environment < ActiveRecord::Base
state :stopped
end
+ def predefined_variables
+ [
+ { key: 'CI_ENVIRONMENT_NAME', value: name, public: true },
+ { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true },
+ ]
+ end
+
+ def recently_updated_on_branch?(ref)
+ ref.to_s == last_deployment.try(:ref)
+ end
+
def last_deployment
deployments.last
end
@@ -63,7 +87,7 @@ class Environment < ActiveRecord::Base
end
def update_merge_request_metrics?
- self.name == "production"
+ (environment_type || name) == "production"
end
def first_deployment_for(commit)
@@ -92,11 +116,60 @@ class Environment < ActiveRecord::Base
def run_stop!(current_user)
return unless available?
- if stop_action.present?
- stop_action.play(current_user)
- else
- stop
- nil
+ stop
+ stop_action.play(current_user)
+ end
+
+ def actions_for(environment)
+ return [] unless manual_actions
+
+ manual_actions.select do |action|
+ action.expanded_environment_name == environment
end
end
+
+ def has_terminals?
+ project.deployment_service.present? && available? && last_deployment.present?
+ end
+
+ def terminals
+ project.deployment_service.terminals(self) if has_terminals?
+ end
+
+ # An environment name is not necessarily suitable for use in URLs, DNS
+ # or other third-party contexts, so provide a slugified version. A slug has
+ # the following properties:
+ # * contains only lowercase letters (a-z), numbers (0-9), and '-'
+ # * begins with a letter
+ # * has a maximum length of 24 bytes (OpenShift limitation)
+ # * cannot end with `-`
+ def generate_slug
+ # Lowercase letters and numbers only
+ slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+
+ # Must start with a letter
+ slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+
+ # Maximum length: 24 characters (OpenShift limitation)
+ slugified = slugified[0..23]
+
+ # Cannot end with a "-" character (Kubernetes label limitation)
+ slugified = slugified[0..-2] if slugified[-1] == "-"
+
+ # Add a random suffix, shortening the current string if necessary, if it
+ # has been slugified. This ensures uniqueness.
+ slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+
+ self.slug = slugified
+ end
+
+ private
+
+ # Slugifying a name may remove the uniqueness guarantee afforded by it being
+ # based on name (which must be unique). To compensate, we add a random
+ # 6-byte suffix in those circumstances. This is not *guaranteed* uniqueness,
+ # but the chance of collisions is vanishingly small
+ def random_suffix
+ (0..5).map { SUFFIX_CHARS.sample }.join
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index c76d88b1c7b..2662f170765 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -43,12 +43,6 @@ class Event < ActiveRecord::Base
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
class << self
- def reset_event_cache_for(target)
- Event.where(target_id: target.id, target_type: target.class.to_s).
- order('id DESC').limit(100).
- update_all(updated_at: Time.now)
- end
-
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
where("action = ? OR (target_type in (?) AND action in (?))",
@@ -62,7 +56,7 @@ class Event < ActiveRecord::Base
end
def visible_to_user?(user = nil)
- if push?
+ if push? || commit_note?
Ability.allowed?(user, :download_code, project)
elsif membership_changed?
true
@@ -283,7 +277,7 @@ class Event < ActiveRecord::Base
end
def commit_note?
- target.for_commit?
+ note? && target && target.for_commit?
end
def issue_note?
@@ -295,7 +289,7 @@ class Event < ActiveRecord::Base
end
def project_snippet_note?
- target.for_snippet?
+ note? && target && target.for_snippet?
end
def note_target
@@ -353,6 +347,10 @@ class Event < ActiveRecord::Base
update_all(last_activity_at: created_at)
end
+ def authored_by?(user)
+ user ? author_id == user.id : false
+ end
+
private
def recent_update?
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 91b508eb325..26712c19b5a 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -38,7 +38,7 @@ class ExternalIssue
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
- def to_reference(_from_project = nil)
+ def to_reference(_from_project = nil, full: nil)
id
end
diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb
index 9803bae0bee..36cf7ad6a28 100644
--- a/app/models/forked_project_link.rb
+++ b/app/models/forked_project_link.rb
@@ -1,4 +1,4 @@
class ForkedProjectLink < ActiveRecord::Base
- belongs_to :forked_to_project, class_name: Project
- belongs_to :forked_from_project, class_name: Project
+ belongs_to :forked_to_project, class_name: 'Project'
+ belongs_to :forked_from_project, class_name: 'Project'
end
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index fa54e3540d0..8867ba0d2ff 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -1,6 +1,10 @@
class GenericCommitStatus < CommitStatus
before_validation :set_default_values
+ validates :target_url, addressable_url: true,
+ length: { maximum: 255 },
+ allow_nil: true
+
# GitHub compatible API
alias_attribute :context, :name
@@ -12,4 +16,10 @@ class GenericCommitStatus < CommitStatus
def tags
[:external]
end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::External::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index cde4a568577..b991d78e27f 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -1,6 +1,8 @@
class GlobalMilestone
include Milestoneish
+ EPOCH = DateTime.parse('1970-01-01')
+
attr_accessor :title, :milestones
alias_attribute :name, :title
@@ -8,13 +10,22 @@ class GlobalMilestone
@first_milestone
end
- def self.build_collection(milestones)
- milestones = milestones.group_by(&:title)
+ def self.build_collection(projects, params)
+ child_milestones = MilestonesFinder.new.execute(projects, params)
- milestones.map do |title, milestones|
- milestones_relation = Milestone.where(id: milestones.map(&:id))
+ milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped|
+ milestones_relation = Milestone.where(id: grouped.map(&:id))
new(title, milestones_relation)
end
+
+ milestones.sort_by { |milestone| milestone.due_date || EPOCH }
+ end
+
+ def self.build(projects, title)
+ child_milestones = Milestone.of_projects(projects).where(title: title)
+ return if child_milestones.blank?
+
+ new(title, child_milestones)
end
def initialize(title, milestones)
@@ -24,30 +35,24 @@ class GlobalMilestone
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end
- def safe_title
- @title.to_slug.normalize.to_s
+ def milestoneish_ids
+ milestones.select(:id)
end
- def expired?
- if due_date
- due_date.past?
- else
- false
- end
+ def safe_title
+ @title.to_slug.normalize.to_s
end
def projects
- @projects ||= Project.for_milestones(milestones.select(:id))
+ @projects ||= Project.for_milestones(milestoneish_ids)
end
def state
- state = milestones.map { |milestone| milestone.state }
-
- if state.count('closed') == state.size
- 'closed'
- else
- 'active'
+ milestones.each do |milestone|
+ return 'active' if milestone.state != 'closed'
end
+
+ 'closed'
end
def active?
@@ -59,11 +64,11 @@ class GlobalMilestone
end
def issues
- @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels)
+ @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels)
end
def merge_requests
- @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels)
+ @merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels)
end
def participants
@@ -81,18 +86,15 @@ class GlobalMilestone
@due_date =
if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
@milestones.first.due_date
- else
- nil
end
end
- def expires_at
- if due_date
- if due_date.past?
- "expired on #{due_date.to_s(:medium)}"
- else
- "expires on #{due_date.to_s(:medium)}"
+ def start_date
+ return @start_date if defined?(@start_date)
+
+ @start_date =
+ if @milestones.all? { |x| x.start_date == @milestones.first.start_date }
+ @milestones.first.start_date
end
- end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index d9e90cd256a..99675ddb366 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -5,6 +5,7 @@ class Group < Namespace
include Gitlab::VisibilityLevel
include AccessRequestable
include Referable
+ include SelectForProjectAuthorization
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :group_members
@@ -47,7 +48,13 @@ class Group < Namespace
end
def sort(method)
- order_by(method)
+ if method == 'storage_size_desc'
+ # storage_size is a virtual column so we need to
+ # pass a string to avoid AR adding the table name
+ reorder('storage_size DESC, namespaces.id DESC')
+ else
+ order_by(method)
+ end
end
def reference_prefix
@@ -61,9 +68,19 @@ class Group < Namespace
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
+
+ def select_for_project_authorization
+ if current_scope.joins_values.include?(:shared_projects)
+ joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
+ .where('project_namespace.share_with_group_lock = ?', false)
+ .select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
+ else
+ super
+ end
+ end
end
- def to_reference(_from_project = nil)
+ def to_reference(_from_project = nil, full: nil)
"#{self.class.reference_prefix}#{name}"
end
@@ -72,7 +89,7 @@ class Group < Namespace
end
def human_name
- name
+ full_name
end
def visibility_level_field
@@ -144,15 +161,17 @@ class Group < Namespace
end
def has_owner?(user)
- owners.include?(user)
+ members_with_parents.owners.where(user_id: user).any?
end
def has_master?(user)
- members.masters.where(user_id: user).any?
+ members_with_parents.masters.where(user_id: user).any?
end
+ # Check if user is a last owner of the group.
+ # Parent owners are ignored for nested groups.
def last_owner?(user)
- has_owner?(user) && owners.size == 1
+ owners.include?(user) && owners.size == 1
end
def avatar_type
@@ -176,4 +195,16 @@ class Group < Namespace
def system_hook_service
SystemHooksService.new
end
+
+ def refresh_members_authorized_projects
+ UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute
+ end
+
+ def members_with_parents
+ GroupMember.where(requested_at: nil, source_id: parents.map(&:id).push(id))
+ end
+
+ def users_with_parents
+ User.where(id: members_with_parents.select(:user_id))
+ end
end
diff --git a/app/models/group_label.rb b/app/models/group_label.rb
index 68841ace2e6..92c83b54861 100644
--- a/app/models/group_label.rb
+++ b/app/models/group_label.rb
@@ -8,8 +8,4 @@ class GroupLabel < Label
def subject_foreign_key
'group_id'
end
-
- def to_reference(source_project = nil, target_project = nil, format: :id)
- super(source_project, target_project, format: format)
- end
end
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
new file mode 100644
index 00000000000..7b6db2634b7
--- /dev/null
+++ b/app/models/group_milestone.rb
@@ -0,0 +1,19 @@
+class GroupMilestone < GlobalMilestone
+ attr_accessor :group
+
+ def self.build_collection(group, projects, params)
+ super(projects, params).each do |milestone|
+ milestone.group = group
+ end
+ end
+
+ def self.build(group, projects, title)
+ super(projects, title).tap do |milestone|
+ milestone.group = group if milestone
+ end
+ end
+
+ def issues_finder_params
+ { group_id: group.id }
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index adbca510ef7..65638d9a299 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -39,6 +39,8 @@ class Issue < ActiveRecord::Base
scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
+ scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
+
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
@@ -60,61 +62,6 @@ class Issue < ActiveRecord::Base
attributes
end
- class << self
- private
-
- # Returns the project that the current scope belongs to if any, nil otherwise.
- #
- # Examples:
- # - my_project.issues.without_due_date.owner_project => my_project
- # - Issue.all.owner_project => nil
- def owner_project
- # No owner if we're not being called from an association
- return unless all.respond_to?(:proxy_association)
-
- owner = all.proxy_association.owner
-
- # Check if the association is or belongs to a project
- if owner.is_a?(Project)
- owner
- else
- begin
- owner.association(:project).target
- rescue ActiveRecord::AssociationNotFoundError
- nil
- end
- end
- end
- end
-
- def self.visible_to_user(user)
- return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
- return all if user.admin?
-
- # Check if we are scoped to a specific project's issues
- if owner_project
- if owner_project.authorized_for_user?(user, Gitlab::Access::REPORTER)
- # If the project is authorized for the user, they can see all issues in the project
- return all
- else
- # else only non confidential and authored/assigned to them
- return where('issues.confidential IS NULL OR issues.confidential IS FALSE
- OR issues.author_id = :user_id OR issues.assignee_id = :user_id',
- user_id: user.id)
- end
- end
-
- where('
- issues.confidential IS NULL
- OR issues.confidential IS FALSE
- OR (issues.confidential = TRUE
- AND (issues.author_id = :user_id
- OR issues.assignee_id = :user_id
- OR issues.project_id IN(:project_ids)))',
- user_id: user.id,
- project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
- end
-
def self.reference_prefix
'#'
end
@@ -150,14 +97,10 @@ class Issue < ActiveRecord::Base
end
end
- def to_reference(from_project = nil)
+ def to_reference(from_project = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
- if cross_project_reference?(from_project)
- reference = project.to_reference + reference
- end
-
- reference
+ "#{project.to_reference(from_project, full: full)}#{reference}"
end
def referenced_merge_requests(current_user = nil)
@@ -182,18 +125,6 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request
end
- # Reset issue events cache
- #
- # Since we do cache @event we need to reset cache in special cases:
- # * when an issue is updated
- # Events cache stored like events/23-20130109142513.
- # The cache key includes updated_at timestamp.
- # Thus it will automatically generate a new fragment
- # when the event is updated because the key changes.
- def reset_events_cache
- Event.reset_event_cache_for(self)
- end
-
# To allow polymorphism with MergeRequest.
def source_project
project
@@ -266,7 +197,7 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
+ json[:subscribed] = subscribed?(options[:user], project) if options.has_key?(:user) && options[:user]
if options.has_key?(:labels)
json[:labels] = labels.as_json(
diff --git a/app/models/key.rb b/app/models/key.rb
index 568a60b8af3..9c74ca84753 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -4,14 +4,24 @@ class Key < ActiveRecord::Base
include AfterCommitQueue
include Sortable
- belongs_to :user
+ LAST_USED_AT_REFRESH_TIME = 1.day.to_i
- before_validation :strip_white_space, :generate_fingerprint
+ belongs_to :user
- validates :title, presence: true, length: { within: 0..255 }
- validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }
- validates :key, format: { without: /\n|\r/, message: 'should be a single line' }
- validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' }
+ before_validation :generate_fingerprint
+
+ validates :title,
+ presence: true,
+ length: { maximum: 255 }
+ validates :key,
+ presence: true,
+ length: { maximum: 5000 },
+ format: { with: /\A(ssh|ecdsa)-.*\Z/ }
+ validates :key,
+ format: { without: /\n|\r/, message: 'should be a single line' }
+ validates :fingerprint,
+ uniqueness: true,
+ presence: { message: 'cannot be generated' }
delegate :name, :email, to: :user, prefix: true
@@ -21,8 +31,9 @@ class Key < ActiveRecord::Base
after_destroy :remove_from_shell
after_destroy :post_destroy_hook
- def strip_white_space
- self.key = key.strip unless key.blank?
+ def key=(value)
+ value.strip! unless value.blank?
+ write_attribute(:key, value)
end
def publishable_key
@@ -40,6 +51,13 @@ class Key < ActiveRecord::Base
"key-#{id}"
end
+ def update_last_used_at
+ lease = Gitlab::ExclusiveLease.new("key_update_last_used_at:#{id}", timeout: LAST_USED_AT_REFRESH_TIME)
+ return unless lease.try_obtain
+
+ UseKeyWorker.perform_async(id)
+ end
+
def add_to_shell
GitlabShellWorker.perform_async(
:add_key,
@@ -48,10 +66,6 @@ class Key < ActiveRecord::Base
)
end
- def notify_user
- run_after_commit { NotificationService.new.new_key(self) }
- end
-
def post_create_hook
SystemHooksService.new.execute_hooks_for(self, :create)
end
@@ -77,4 +91,8 @@ class Key < ActiveRecord::Base
self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
end
+
+ def notify_user
+ run_after_commit { NotificationService.new.new_key(self) }
+ end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index d9287f2dc29..5b6b9a7a736 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -26,6 +26,7 @@ class Label < ActiveRecord::Base
# Don't allow ',' for label titles
validates :title, presence: true, format: { with: /\A[^,]+\z/ }
validates :title, uniqueness: { scope: [:group_id, :project_id] }
+ validates :title, length: { maximum: 255 }
default_scope { order(title: :asc) }
@@ -144,18 +145,19 @@ class Label < ActiveRecord::Base
#
# Examples:
#
- # Label.first.to_reference # => "~1"
- # Label.first.to_reference(format: :name) # => "~\"bug\""
- # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1"
+ # Label.first.to_reference # => "~1"
+ # Label.first.to_reference(format: :name) # => "~\"bug\""
+ # Label.first.to_reference(project, target_project: same_namespace_project) # => "gitlab-ce~1"
+ # Label.first.to_reference(project, target_project: another_namespace_project) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
#
- def to_reference(source_project = nil, target_project = nil, format: :id)
+ def to_reference(from_project = nil, target_project: nil, format: :id, full: false)
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- if cross_project_reference?(source_project, target_project)
- source_project.to_reference + reference
+ if from_project
+ "#{from_project.to_reference(target_project, full: full)}#{reference}"
else
reference
end
@@ -169,10 +171,6 @@ class Label < ActiveRecord::Base
private
- def cross_project_reference?(source_project, target_project)
- source_project && target_project && source_project != target_project
- end
-
def issues_count(user, params = {})
params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all')
IssuesFinder.new(user, params.with_indifferent_access).execute.count
diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb
index 0fd5f089db9..007eed5600a 100644
--- a/app/models/lfs_objects_project.rb
+++ b/app/models/lfs_objects_project.rb
@@ -5,4 +5,13 @@ class LfsObjectsProject < ActiveRecord::Base
validates :lfs_object_id, presence: true
validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
validates :project_id, presence: true
+
+ after_create :update_project_statistics
+ after_destroy :update_project_statistics
+
+ private
+
+ def update_project_statistics
+ ProjectCacheWorker.perform_async(project_id, [], [:lfs_objects_size])
+ end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index b89ba8ecbb8..c585e0b450e 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -57,12 +57,18 @@ class Member < ActiveRecord::Base
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) }
+ scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
+ scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
+ scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
+ scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
+
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?, unless: :importing?
after_create :send_request, if: :request?, unless: :importing?
after_create :create_notification_setting, unless: [:pending?, :importing?]
after_create :post_create_hook, unless: [:pending?, :importing?]
+ after_create :refresh_member_authorized_projects, if: :importing?
after_update :post_update_hook, unless: [:pending?, :importing?]
after_destroy :post_destroy_hook, unless: :pending?
@@ -71,6 +77,34 @@ class Member < ActiveRecord::Base
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self
+ def search(query)
+ joins(:user).merge(User.search(query))
+ end
+
+ def sort(method)
+ case method.to_s
+ when 'access_level_asc' then reorder(access_level: :asc)
+ when 'access_level_desc' then reorder(access_level: :desc)
+ when 'recent_sign_in' then order_recent_sign_in
+ when 'oldest_sign_in' then order_oldest_sign_in
+ when 'last_joined' then order_created_desc
+ when 'oldest_joined' then order_created_asc
+ else
+ order_by(method)
+ end
+ end
+
+ def left_join_users
+ users = User.arel_table
+ members = Member.arel_table
+
+ member_users = members.join(users, Arel::Nodes::OuterJoin).
+ on(members[:user_id].eq(users[:id])).
+ join_sources
+
+ joins(member_users)
+ end
+
def access_for_user_ids(user_ids)
where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
end
@@ -88,8 +122,8 @@ class Member < ActiveRecord::Base
member =
if user.is_a?(User)
source.members.find_by(user_id: user.id) ||
- source.requesters.find_by(user_id: user.id) ||
- source.members.build(user_id: user.id)
+ source.requesters.find_by(user_id: user.id) ||
+ source.members.build(user_id: user.id)
else
source.members.build(invite_email: user)
end
@@ -113,6 +147,8 @@ class Member < ActiveRecord::Base
member.save
end
+ UserProjectAccessChangedService.new(user.id).execute if user.is_a?(User)
+
member
end
@@ -239,17 +275,28 @@ class Member < ActiveRecord::Base
end
def post_create_hook
+ UserProjectAccessChangedService.new(user.id).execute
system_hook_service.execute_hooks_for(self, :create)
end
def post_update_hook
- # override in subclass
+ UserProjectAccessChangedService.new(user.id).execute if access_level_changed?
end
def post_destroy_hook
+ refresh_member_authorized_projects
system_hook_service.execute_hooks_for(self, :destroy)
end
+ def refresh_member_authorized_projects
+ # If user/source is being destroyed, project access are gonna be destroyed eventually
+ # because of DB foreign keys, so we shouldn't bother with refreshing after each
+ # member is destroyed through association
+ return if destroyed_by_association.present?
+
+ UserProjectAccessChangedService.new(user_id).execute
+ end
+
def after_accept_invite
post_create_hook
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d76feb9680e..cd5b345bae5 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -22,7 +22,8 @@ class MergeRequest < ActiveRecord::Base
after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed
- delegate :commits, :real_size, to: :merge_request_diff, prefix: nil
+ delegate :commits, :real_size, :commits_sha, :commits_count,
+ to: :merge_request_diff, prefix: nil
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -90,17 +91,23 @@ class MergeRequest < ActiveRecord::Base
around_transition do |merge_request, transition, block|
Gitlab::Timeless.timeless(merge_request, &block)
end
+
+ after_transition unchecked: :cannot_be_merged do |merge_request, transition|
+ TodoService.new.merge_request_became_unmergeable(merge_request)
+ end
end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
- validates :merge_user, presence: true, if: :merge_when_build_succeeds?
+ validates :merge_user, presence: true, if: :merge_when_build_succeeds?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
- scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
+ scope :by_source_or_target_branch, ->(branch_name) do
+ where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
+ end
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
@@ -172,14 +179,10 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
- def to_reference(from_project = nil)
+ def to_reference(from_project = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
- if cross_project_reference?(from_project)
- reference = project.to_reference + reference
- end
-
- reference
+ "#{project.to_reference(from_project, full: full)}#{reference}"
end
def first_commit
@@ -199,7 +202,9 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
- diffs(diff_options).size
+ opts = diff_options || {}
+
+ raw_diffs(opts).size
end
def diff_base_commit
@@ -220,7 +225,7 @@ class MergeRequest < ActiveRecord::Base
# true base commit, so we can't simply have `#diff_base_commit` fall back on
# this method.
def likely_diff_base_commit
- first_commit.parent || first_commit
+ first_commit.try(:parent) || first_commit
end
def diff_start_commit
@@ -453,7 +458,7 @@ class MergeRequest < ActiveRecord::Base
should_remove_source_branch? || force_remove_source_branch?
end
- def mr_and_commit_notes
+ def related_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
commit_ids = commits.last(commits_for_notes_limit).map(&:id)
@@ -469,7 +474,7 @@ class MergeRequest < ActiveRecord::Base
end
def discussions
- @discussions ||= self.mr_and_commit_notes.
+ @discussions ||= self.related_notes.
inc_relations_for_view.
fresh.
discussions
@@ -479,6 +484,14 @@ class MergeRequest < ActiveRecord::Base
@diff_discussions ||= self.notes.diff_notes.discussions
end
+ def resolvable_discussions
+ @resolvable_discussions ||= diff_discussions.select(&:to_be_resolved?)
+ end
+
+ def discussions_can_be_resolved_by?(user)
+ resolvable_discussions.all? { |discussion| discussion.can_resolve?(user) }
+ end
+
def find_diff_discussion(discussion_id)
notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a
return if notes.empty?
@@ -494,10 +507,14 @@ class MergeRequest < ActiveRecord::Base
discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
end
+ def discussions_to_be_resolved?
+ discussions_resolvable? && !discussions_resolved?
+ end
+
def mergeable_discussions_state?
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
- discussions_resolved?
+ !discussions_to_be_resolved?
end
def hook_attrs
@@ -557,6 +574,15 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def issues_mentioned_but_not_closing(current_user = self.author)
+ return [] unless target_branch == project.default_branch
+
+ ext = Gitlab::ReferenceExtractor.new(project, current_user)
+ ext.analyze(description)
+
+ ext.issues - closes_issues
+ end
+
def target_project_path
if target_project
target_project.path_with_namespace
@@ -601,25 +627,24 @@ class MergeRequest < ActiveRecord::Base
self.target_project.repository.branch_names.include?(self.target_branch)
end
- # Reset merge request events cache
- #
- # Since we do cache @event we need to reset cache in special cases:
- # * when a merge request is updated
- # Events cache stored like events/23-20130109142513.
- # The cache key includes updated_at timestamp.
- # Thus it will automatically generate a new fragment
- # when the event is updated because the key changes.
- def reset_events_cache
- Event.reset_event_cache_for(self)
- end
-
- def merge_commit_message
- message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
- message << "#{title}\n\n"
- message << "#{description}\n\n" if description.present?
+ def merge_commit_message(include_description: false)
+ closes_issues_references = closes_issues.map do |issue|
+ issue.to_reference(target_project)
+ end
+
+ message = [
+ "Merge branch '#{source_branch}' into '#{target_branch}'",
+ title
+ ]
+
+ if !include_description && closes_issues_references.present?
+ message << "Closes #{closes_issues_references.to_sentence}"
+ end
+
+ message << "#{description}" if include_description && description.present?
message << "See merge request #{to_reference}"
- message
+ message.join("\n\n")
end
def reset_merge_when_build_succeeds
@@ -670,7 +695,7 @@ class MergeRequest < ActiveRecord::Base
end
def broken?
- self.commits.blank? || branch_missing? || cannot_be_merged?
+ has_no_commits? || branch_missing? || cannot_be_merged?
end
def can_be_merged_by?(user)
@@ -686,18 +711,21 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state?
return true unless project.only_allow_merge_if_build_succeeds?
- !pipeline || pipeline.success?
+ !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
end
def environments
return [] unless diff_head_commit
- @environments ||=
- begin
- envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true)
- envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project
- envs.uniq
- end
+ @environments ||= begin
+ target_envs = target_project.environments_for(
+ target_branch, commit: diff_head_commit, with_tags: true)
+
+ source_envs = source_project.environments_for(
+ source_branch, commit: diff_head_commit) if source_project
+
+ (target_envs.to_a + source_envs.to_a).uniq
+ end
end
def state_human_name
@@ -775,18 +803,14 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
end
- def commits_sha
- commits.map(&:sha)
- end
-
- def pipeline
+ def head_pipeline
return unless diff_head_sha && source_project
- @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
+ @head_pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
end
def all_pipelines
- return unless source_project
+ return Ci::Pipeline.none unless source_project
@all_pipelines ||= source_project.pipelines
.where(sha: all_commits_sha, ref: source_branch)
@@ -809,7 +833,7 @@ class MergeRequest < ActiveRecord::Base
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end
- def can_be_reverted?(current_user = nil)
+ def can_be_reverted?(current_user)
merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end
@@ -876,4 +900,24 @@ class MergeRequest < ActiveRecord::Base
@conflicts_can_be_resolved_in_ui = false
end
end
+
+ def has_commits?
+ merge_request_diff && commits_count > 0
+ end
+
+ def has_no_commits?
+ !has_commits?
+ end
+
+ def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
+ return false unless can_be_merged_by?(current_user)
+
+ return true if autocomplete_precheck
+
+ return false unless mergeable?(skip_ci_check: true)
+ return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
+ return false if last_diff_sha != diff_head_sha
+
+ true
+ end
end
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index 99c49a020c9..cdc408738be 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -1,5 +1,6 @@
class MergeRequest::Metrics < ActiveRecord::Base
belongs_to :merge_request
+ belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
def record!
if merge_request.merged? && self.merged_at.blank?
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index dd65a9a8b86..dadb81f9b6e 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,7 +1,7 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
include Importable
- include EncodingHelper
+ include Gitlab::Git::EncodingHelper
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
@@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request
+ serialize :st_commits
+ serialize :st_diffs
+
state_machine :state, initial: :empty do
state :collected
state :overflow
@@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base
state :overflow_diff_lines_limit
end
- serialize :st_commits
- serialize :st_diffs
+ scope :viewable, -> { without_state(:empty) }
# All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff.
@@ -125,11 +127,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commits_sha
- if @commits
- commits.map(&:sha)
- else
- st_commits.map { |commit| commit[:id] }
- end
+ st_commits.map { |commit| commit[:id] }
end
def diff_refs
@@ -174,6 +172,10 @@ class MergeRequestDiff < ActiveRecord::Base
CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight)
end
+ def commits_count
+ st_commits.count
+ end
+
private
# Old GitLab implementations may have generated diffs as ["--broken-diff"].
@@ -232,28 +234,28 @@ class MergeRequestDiff < ActiveRecord::Base
# and save it as array of hashes in st_diffs db field
def save_diffs
new_attributes = {}
- new_diffs = []
if commits.size.zero?
new_attributes[:state] = :empty
else
diff_collection = compare.diffs(Commit.max_diff_options)
-
- if diff_collection.overflow?
- # Set our state to 'overflow' to make the #empty? and #collected?
- # methods (generated by StateMachine) return false.
- new_attributes[:state] = :overflow
- end
-
- new_attributes[:real_size] = diff_collection.real_size
+ new_attributes[:real_size] = compare.diffs.real_size
if diff_collection.any?
new_diffs = dump_diffs(diff_collection)
new_attributes[:state] = :collected
end
+
+ new_attributes[:st_diffs] = new_diffs || []
+
+ # Set our state to 'overflow' to make the #empty? and #collected?
+ # methods (generated by StateMachine) return false.
+ #
+ # This attribution has to come at the end of the method so 'overflow'
+ # state does not get overridden by 'collected'.
+ new_attributes[:state] = :overflow if diff_collection.overflow?
end
- new_attributes[:st_diffs] = new_diffs
update_columns_serialized(new_attributes)
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 23aecbfa3a6..7331000a9f2 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -29,6 +29,7 @@ class Milestone < ActiveRecord::Base
validates :title, presence: true, uniqueness: { scope: :project_id }
validates :project, presence: true
+ validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? }
strip_attributes :title
@@ -112,41 +113,24 @@ class Milestone < ActiveRecord::Base
#
# Examples:
#
- # Milestone.first.to_reference # => "%1"
- # Milestone.first.to_reference(format: :name) # => "%\"goal\""
- # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1"
+ # Milestone.first.to_reference # => "%1"
+ # Milestone.first.to_reference(format: :name) # => "%\"goal\""
+ # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
+ # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
- def to_reference(from_project = nil, format: :iid)
+ def to_reference(from_project = nil, format: :iid, full: false)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- if cross_project_reference?(from_project)
- project.to_reference + reference
- else
- reference
- end
+ "#{project.to_reference(from_project, full: full)}#{reference}"
end
def reference_link_text(from_project = nil)
self.title
end
- def expired?
- if due_date
- due_date.past?
- else
- false
- end
- end
-
- def expires_at
- if due_date
- if due_date.past?
- "expired on #{due_date.to_s(:medium)}"
- else
- "expires on #{due_date.to_s(:medium)}"
- end
- end
+ def milestoneish_ids
+ id
end
def can_be_closed?
@@ -212,4 +196,14 @@ class Milestone < ActiveRecord::Base
def sanitize_title(value)
CGI.unescape_html(Sanitize.clean(value.to_s))
end
+
+ def start_date_should_be_less_than_due_date
+ if due_date <= start_date
+ errors.add(:start_date, "Can't be greater than due date")
+ end
+ end
+
+ def issues_finder_params
+ { project_id: project_id }
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index b67049f0f55..dd33975731f 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -4,29 +4,34 @@ class Namespace < ActiveRecord::Base
include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
+ include Routable
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy
+ has_many :project_statistics
belongs_to :owner, class_name: "User"
+ belongs_to :parent, class_name: "Namespace"
+ has_many :children, class_name: "Namespace", foreign_key: :parent_id
+
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
- length: { within: 0..255 },
- namespace_name: true,
presence: true,
- uniqueness: true
+ uniqueness: { scope: :parent_id },
+ length: { maximum: 255 },
+ namespace_name: true
- validates :description, length: { within: 0..255 }
+ validates :description, length: { maximum: 255 }
validates :path,
- length: { within: 1..255 },
- namespace: true,
presence: true,
- uniqueness: { case_sensitive: false }
+ length: { maximum: 255 },
+ namespace: true
delegate :name, to: :owner, allow_nil: true, prefix: true
after_update :move_dir, if: :path_changed?
+ after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
# Save the storage paths before the projects are destroyed to use them on after destroy
before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths }
@@ -34,6 +39,18 @@ class Namespace < ActiveRecord::Base
scope :root, -> { where('type IS NULL') }
+ scope :with_statistics, -> do
+ joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
+ .group('namespaces.id')
+ .select(
+ 'namespaces.*',
+ 'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
+ 'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
+ 'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
+ 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
+ )
+ end
+
class << self
def by_path(path)
find_by('lower(path) = :value', value: path.downcase)
@@ -85,7 +102,7 @@ class Namespace < ActiveRecord::Base
end
def to_param
- path
+ full_path
end
def human_name
@@ -94,7 +111,7 @@ class Namespace < ActiveRecord::Base
def move_dir
if any_project_has_container_registry_tags?
- raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry')
+ raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
end
# Move the namespace directory in all storages paths used by member projects
@@ -103,14 +120,18 @@ class Namespace < ActiveRecord::Base
gitlab_shell.add_namespace(repository_storage_path, path_was)
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
+ Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
- raise Exception.new('namespace directory cannot be moved')
+ raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
end
end
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ remove_exports!
+
# If repositories moved successfully we need to
# send update instructions to users.
# However we cannot allow rollback since we moved namespace dir
@@ -147,6 +168,27 @@ class Namespace < ActiveRecord::Base
Gitlab.config.lfs.enabled
end
+ def full_path
+ if parent
+ parent.full_path + '/' + path
+ else
+ path
+ end
+ end
+
+ def full_name
+ @full_name ||=
+ if parent
+ parent.full_name + ' / ' + name
+ else
+ name
+ end
+ end
+
+ def parents
+ @parents ||= parent ? parent.parents + [parent] : []
+ end
+
private
def repository_storage_paths
@@ -174,5 +216,34 @@ class Namespace < ActiveRecord::Base
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
end
end
+
+ remove_exports!
+ end
+
+ def refresh_access_of_projects_invited_groups
+ Group.
+ joins(project_group_links: :project).
+ where(projects: { namespace_id: id }).
+ find_each(&:refresh_members_authorized_projects)
+ end
+
+ def full_path_changed?
+ path_changed? || parent_id_changed?
+ end
+
+ def remove_exports!
+ Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
+ end
+
+ def export_path
+ File.join(Gitlab::ImportExport.storage_path, full_path_was)
+ end
+
+ def full_path_was
+ if parent
+ parent.full_path + '/' + path_was
+ else
+ path_was
+ end
end
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 345041a6ad1..b524ca50ee8 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -161,8 +161,8 @@ module Network
def is_overlap?(range, overlap_space)
range.each do |i|
if i != range.first &&
- i != range.last &&
- @commits[i].spaces.include?(overlap_space)
+ i != range.last &&
+ @commits[i].spaces.include?(overlap_space)
return true
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 2d644b03e4d..0c1b05dabf2 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -7,6 +7,7 @@ class Note < ActiveRecord::Base
include Importable
include FasterCacheKeys
include CacheMarkdownField
+ include AfterCommitQueue
cache_markdown_field :note, pipeline: :note
@@ -18,6 +19,9 @@ class Note < ActiveRecord::Base
# Banzai::ObjectRenderer
attr_accessor :user_visible_reference_count
+ # Attribute used to store the attributes that have ben changed by slash commands.
+ attr_accessor :commands_changes
+
default_value_for :system, false
attr_mentionable :note, pipeline: :note
@@ -95,7 +99,7 @@ class Note < ActiveRecord::Base
end
def discussions
- Discussion.for_notes(all)
+ Discussion.for_notes(fresh)
end
def grouped_diff_discussions
@@ -103,23 +107,6 @@ class Note < ActiveRecord::Base
Discussion.for_diff_notes(active_notes).
map { |d| [d.line_code, d] }.to_h
end
-
- # Searches for notes matching the given query.
- #
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
- #
- # query - The search query as a String.
- # as_user - Limit results to those viewable by a specific user
- #
- # Returns an ActiveRecord::Relation.
- def search(query, as_user: nil)
- table = arel_table
- pattern = "%#{query}%"
-
- Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
- where(table[:note].matches(pattern)).
- merge(Issue.visible_to_user(as_user))
- end
end
def cross_reference?
@@ -197,19 +184,6 @@ class Note < ActiveRecord::Base
super(noteable_type.to_s.classify.constantize.base_class.to_s)
end
- # Reset notes events cache
- #
- # Since we do cache @event we need to reset cache in special cases:
- # * when a note is updated
- # * when a note is removed
- # Events cache stored like events/23-20130109142513.
- # The cache key includes updated_at timestamp.
- # Thus it will automatically generate a new fragment
- # when the event is updated because the key changes.
- def reset_events_cache
- Event.reset_event_cache_for(self)
- end
-
def editable?
!system?
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 43fc218de2b..58f6214bea7 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline
]
+ EXCLUDED_WATCHER_EVENTS = [
+ :success_pipeline
+ ]
+
store :events, accessors: EMAIL_EVENTS, coder: JSON
before_create :set_events
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index c4b095e0c04..10a34c42fd8 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -2,6 +2,8 @@ class PersonalAccessToken < ActiveRecord::Base
include TokenAuthenticatable
add_authentication_token_field :token
+ serialize :scopes, Array
+
belongs_to :user
scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
diff --git a/app/models/project.rb b/app/models/project.rb
index bbe590b5a8a..cd35601d76b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -12,7 +12,10 @@ class Project < ActiveRecord::Base
include AfterCommitQueue
include CaseSensitivity
include TokenAuthenticatable
+ include ValidAttribute
include ProjectFeaturesCompatibility
+ include SelectForProjectAuthorization
+ include Routable
extend Gitlab::ConfigHelper
@@ -23,7 +26,9 @@ class Project < ActiveRecord::Base
cache_markdown_field :description, pipeline: :description
- delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
+ delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
+ :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
+ allow_nil: true
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
@@ -35,10 +40,12 @@ class Project < ActiveRecord::Base
default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
+ default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature
after_save :ensure_dir_exist, if: :namespace_id_changed?
+ after_save :update_project_statistics, if: :namespace_id_changed?
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
@@ -59,6 +66,8 @@ class Project < ActiveRecord::Base
end
end
+ after_validation :check_pending_delete
+
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
@@ -76,7 +85,6 @@ class Project < ActiveRecord::Base
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
# Project services
- has_many :services
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
@@ -89,6 +97,9 @@ class Project < ActiveRecord::Base
has_one :assembla_service, dependent: :destroy
has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy
+ has_one :mattermost_slash_commands_service, dependent: :destroy
+ has_one :mattermost_service, dependent: :destroy
+ has_one :slack_slash_commands_service, dependent: :destroy
has_one :slack_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
@@ -100,6 +111,7 @@ class Project < ActiveRecord::Base
has_one :bugzilla_service, dependent: :destroy
has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy
+ has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
@@ -109,8 +121,6 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it
has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id'
- # Merge requests from source project should be kept when source project was removed
- has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest
has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy
@@ -121,6 +131,8 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
+ has_many :project_authorizations
+ has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :project_members
has_many :users, through: :project_members
@@ -141,6 +153,7 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy
+ has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
@@ -158,18 +171,20 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, to: :team
+ delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
# Validations
validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true
validates :name,
presence: true,
- length: { within: 0..255 },
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.project_name_regex,
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
- length: { within: 0..255 },
+ project_path: true,
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
validates :namespace, presence: true
@@ -208,6 +223,7 @@ class Project < ActiveRecord::Base
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
+ scope :with_statistics, -> { includes(:statistics) }
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
@@ -315,94 +331,15 @@ class Project < ActiveRecord::Base
non_archived.where(table[:name].matches(pattern))
end
- # Finds a single project for the given path.
- #
- # path - The full project path (including namespace path).
- #
- # Returns a Project, or nil if no project could be found.
- def find_with_namespace(path)
- namespace_path, project_path = path.split('/', 2)
-
- return unless namespace_path && project_path
-
- namespace_path = connection.quote(namespace_path)
- project_path = connection.quote(project_path)
-
- # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
- # any literal matches come first, for this we have to use "BINARY".
- # Without this there's still no guarantee in what order MySQL will return
- # rows.
- binary = Gitlab::Database.mysql? ? 'BINARY' : ''
-
- order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \
- "AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)"
-
- where_paths_in([path]).reorder(order_sql).take
- end
-
- # Builds a relation to find multiple projects by their full paths.
- #
- # Each path must be in the following format:
- #
- # namespace_path/project_path
- #
- # For example:
- #
- # gitlab-org/gitlab-ce
- #
- # Usage:
- #
- # Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
- #
- # This would return the projects with the full paths matching the values
- # given.
- #
- # paths - An Array of full paths (namespace path + project path) for which
- # to find the projects.
- #
- # Returns an ActiveRecord::Relation.
- def where_paths_in(paths)
- wheres = []
- cast_lower = Gitlab::Database.postgresql?
-
- paths.each do |path|
- namespace_path, project_path = path.split('/', 2)
-
- next unless namespace_path && project_path
-
- namespace_path = connection.quote(namespace_path)
- project_path = connection.quote(project_path)
-
- where = "(namespaces.path = #{namespace_path}
- AND projects.path = #{project_path})"
-
- if cast_lower
- where = "(
- #{where}
- OR (
- LOWER(namespaces.path) = LOWER(#{namespace_path})
- AND LOWER(projects.path) = LOWER(#{project_path})
- )
- )"
- end
-
- wheres << where
- end
-
- if wheres.empty?
- none
- else
- joins(:namespace).where(wheres.join(' OR '))
- end
- end
-
def visibility_levels
Gitlab::VisibilityLevel.options
end
def sort(method)
- if method == 'repository_size_desc'
- reorder(repository_size: :desc, id: :desc)
+ if method == 'storage_size_desc'
+ # storage_size is a joined column so we need to
+ # pass a string to avoid AR adding the table name
+ reorder('project_statistics.storage_size DESC, projects.id DESC')
else
order_by(method)
end
@@ -410,7 +347,11 @@ class Project < ActiveRecord::Base
def reference_pattern
name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR
- %r{(?<project>#{name_pattern}/#{name_pattern})}
+
+ %r{
+ ((?<namespace>#{name_pattern})\/)?
+ (?<project>#{name_pattern})
+ }x
end
def trending
@@ -427,6 +368,10 @@ class Project < ActiveRecord::Base
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end
+
+ # Add alias for Routable method for compatibility with old code.
+ # In future all calls `find_with_namespace` should be replaced with `find_by_full_path`
+ alias_method :find_with_namespace, :find_by_full_path
end
def lfs_enabled?
@@ -594,6 +539,10 @@ class Project < ActiveRecord::Base
import_type == 'gitlab_project'
end
+ def gitea_import?
+ import_type == 'gitea'
+ end
+
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
projects_limit = creator.projects_limit
@@ -641,18 +590,26 @@ class Project < ActiveRecord::Base
end
end
- def to_reference(_from_project = nil)
- path_with_namespace
+ def to_reference(from_project = nil, full: false)
+ if full || cross_namespace_reference?(from_project)
+ path_with_namespace
+ elsif cross_project_reference?(from_project)
+ path
+ end
+ end
+
+ def to_human_reference(from_project = nil)
+ if cross_namespace_reference?(from_project)
+ name_with_namespace
+ elsif cross_project_reference?(from_project)
+ name
+ end
end
def web_url
Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self)
end
- def web_url_without_protocol
- web_url.split('://')[1]
- end
-
def new_issue_address(author)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
@@ -678,9 +635,9 @@ class Project < ActiveRecord::Base
self.id
end
- def get_issue(issue_id)
+ def get_issue(issue_id, current_user)
if default_issues_tracker?
- issues.find_by(iid: issue_id)
+ IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id)
else
ExternalIssue.new(issue_id, self)
end
@@ -748,27 +705,32 @@ class Project < ActiveRecord::Base
update_column(:has_external_wiki, services.external_wikis.any?)
end
- def build_missing_services
+ def find_or_initialize_services
services_templates = Service.where(template: true)
- Service.available_services_names.each do |service_name|
+ Service.available_services_names.map do |service_name|
service = find_service(services, service_name)
- # If service is available but missing in db
- if service.nil?
+ if service
+ service
+ else
# We should check if template for the service exists
template = find_service(services_templates, service_name)
if template.nil?
- # If no template, we should create an instance. Ex `create_gitlab_ci_service`
- public_send("create_#{service_name}_service")
+ # If no template, we should create an instance. Ex `build_gitlab_ci_service`
+ public_send("build_#{service_name}_service")
else
- Service.create_from_template(self.id, template)
+ Service.build_from_template(id, template)
end
end
end
end
+ def find_or_initialize_service(name)
+ find_or_initialize_services.find { |service| service.to_param == name }
+ end
+
def create_labels
Label.templates.each do |label|
params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
@@ -788,6 +750,14 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
+ def deployment_services
+ services.where(category: :deployment)
+ end
+
+ def deployment_service
+ @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
+ end
+
def jira_tracker?
issues_tracker.to_param == 'jira'
end
@@ -849,13 +819,14 @@ class Project < ActiveRecord::Base
end
alias_method :human_name, :name_with_namespace
- def path_with_namespace
- if namespace
- namespace.path + '/' + path
+ def full_path
+ if namespace && path
+ namespace.full_path + '/' + path
else
path
end
end
+ alias_method :path_with_namespace, :full_path
def execute_hooks(data, hooks_scope = :push_hooks)
hooks.send(hooks_scope).each do |hook|
@@ -878,7 +849,7 @@ class Project < ActiveRecord::Base
end
def empty_repo?
- !repository.exists? || !repository.has_visible_content?
+ repository.empty_repo?
end
def repo
@@ -952,7 +923,7 @@ class Project < ActiveRecord::Base
Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present"
# we currently doesn't support renaming repository if it contains tags in container registry
- raise Exception.new('Project cannot be renamed, because tags are present in its container registry')
+ raise StandardError.new('Project cannot be renamed, because tags are present in its container registry')
end
if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace)
@@ -962,7 +933,6 @@ class Project < ActiveRecord::Base
begin
gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace)
- reset_events_cache
@old_path_with_namespace = old_path_with_namespace
@@ -980,7 +950,7 @@ class Project < ActiveRecord::Base
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
- raise Exception.new('repository cannot be renamed')
+ raise StandardError.new('repository cannot be renamed')
end
Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
@@ -1029,22 +999,6 @@ class Project < ActiveRecord::Base
attrs
end
- # Reset events cache related to this project
- #
- # Since we do cache @event we need to reset cache in special cases:
- # * when project was moved
- # * when project was renamed
- # * when the project avatar changes
- # Events cache stored like events/23-20130109142513.
- # The cache key includes updated_at timestamp.
- # Thus it will automatically generate a new fragment
- # when the event is updated because the key changes.
- def reset_events_cache
- Event.where(project_id: self.id).
- order('id DESC').limit(100).
- update_all(updated_at: Time.now)
- end
-
def project_member(user)
project_members.find_by(user_id: user)
end
@@ -1076,7 +1030,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch)
- repository.expire_avatar_cache(branch)
+ repository.after_change_head
reload_default_branch
end
@@ -1084,14 +1038,6 @@ class Project < ActiveRecord::Base
forked? && project == forked_from_project
end
- def update_repository_size
- update_attribute(:repository_size, repository.size)
- end
-
- def update_commit_count
- update_attribute(:commit_count, repository.commit_count)
- end
-
def forks_count
forks.count
end
@@ -1282,18 +1228,10 @@ class Project < ActiveRecord::Base
end
end
- # Checks if `user` is authorized for this project, with at least the
- # `min_access_level` (if given).
- #
- # If you change the logic of this method, please also update `User#authorized_projects`
- def authorized_for_user?(user, min_access_level = nil)
- return false unless user
+ def deployment_variables
+ return [] unless deployment_service
- return true if personal? && namespace_id == user.namespace_id
-
- authorized_for_user_by_group?(user, min_access_level) ||
- authorized_for_user_by_members?(user, min_access_level) ||
- authorized_for_user_by_shared_projects?(user, min_access_level)
+ deployment_service.predefined_variables
end
def append_or_update_attribute(name, value)
@@ -1318,65 +1256,86 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
- def environments_for(ref, commit, with_tags: false)
- environment_ids = deployments.group(:environment_id).
- select(:environment_id)
+ def environments_for(ref, commit: nil, with_tags: false)
+ deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?'
- environment_ids =
- if with_tags
- environment_ids.where('ref=? OR tag IS TRUE', ref)
- else
- environment_ids.where(ref: ref)
- end
+ environment_ids = deployments
+ .where(deployments_query, ref.to_s)
+ .group(:environment_id)
+ .select(:environment_id)
+
+ environments_found = environments.available
+ .where(id: environment_ids).to_a
+
+ return environments_found unless commit
- environments.available.where(id: environment_ids).select do |environment|
+ environments_found.select do |environment|
environment.includes_commit?(commit)
end
end
+ def environments_recently_updated_on_branch(branch)
+ environments_for(branch).select do |environment|
+ environment.recently_updated_on_branch?(branch)
+ end
+ end
+
private
+ # Check if a reference is being done cross-project
+ #
+ # from_project - Refering Project object
+ def cross_project_reference?(from_project)
+ from_project && self != from_project
+ end
+
def pushes_since_gc_redis_key
"projects/#{id}/pushes_since_gc"
end
+ def cross_namespace_reference?(from_project)
+ from_project && namespace != from_project.namespace
+ end
+
def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
end
- def authorized_for_user_by_group?(user, min_access_level)
- member = user.group_members.find_by(source_id: group)
-
- member && (!min_access_level || member.access_level >= min_access_level)
+ # Similar to the normal callbacks that hook into the life cycle of an
+ # Active Record object, you can also define callbacks that get triggered
+ # when you add an object to an association collection. If any of these
+ # callbacks throw an exception, the object will not be added to the
+ # collection. Before you add a new board to the boards collection if you
+ # already have 1, 2, or n it will fail, but it if you have 0 that is lower
+ # than the number of permitted boards per project it won't fail.
+ def validate_board_limit(board)
+ raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
end
- def authorized_for_user_by_members?(user, min_access_level)
- member = members.find_by(user_id: user)
+ def full_path_changed?
+ path_changed? || namespace_id_changed?
+ end
- member && (!min_access_level || member.access_level >= min_access_level)
+ def update_project_statistics
+ stats = statistics || build_statistics
+ stats.update(namespace_id: namespace_id)
end
- def authorized_for_user_by_shared_projects?(user, min_access_level)
- shared_projects = user.group_members.joins(group: :shared_projects).
- where(project_group_links: { project_id: self })
+ def check_pending_delete
+ return if valid_attribute?(:name) && valid_attribute?(:path)
+ return unless pending_delete_twin
- if min_access_level
- members_scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } }
- shared_projects = shared_projects.where(members: members_scope)
+ %i[route route.path name path].each do |error|
+ errors.delete(error)
end
- shared_projects.any?
+ errors.add(:base, "The project is still being deleted. Please try again later.")
end
- # Similar to the normal callbacks that hook into the life cycle of an
- # Active Record object, you can also define callbacks that get triggered
- # when you add an object to an association collection. If any of these
- # callbacks throw an exception, the object will not be added to the
- # collection. Before you add a new board to the boards collection if you
- # already have 1, 2, or n it will fail, but it if you have 0 that is lower
- # than the number of permitted boards per project it won't fail.
- def validate_board_limit(board)
- raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
+ def pending_delete_twin
+ return false unless path
+
+ Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace)
end
end
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
new file mode 100644
index 00000000000..4c7f4f5a429
--- /dev/null
+++ b/app/models/project_authorization.rb
@@ -0,0 +1,21 @@
+class ProjectAuthorization < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :project
+
+ validates :project, presence: true
+ validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
+ validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
+
+ def self.insert_authorizations(rows, per_batch = 1000)
+ rows.each_slice(per_batch) do |slice|
+ tuples = slice.map do |tuple|
+ tuple.map { |value| connection.quote(value) }
+ end
+
+ connection.execute <<-EOF.strip_heredoc
+ INSERT INTO project_authorizations (user_id, project_id, access_level)
+ VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ EOF
+ end
+ end
+end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 34fd5a57b5e..03194fc2141 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -49,23 +49,21 @@ class ProjectFeature < ActiveRecord::Base
end
def builds_enabled?
- return true unless builds_access_level
-
builds_access_level > DISABLED
end
def wiki_enabled?
- return true unless wiki_access_level
-
wiki_access_level > DISABLED
end
def merge_requests_enabled?
- return true unless merge_requests_access_level
-
merge_requests_access_level > DISABLED
end
+ def issues_enabled?
+ issues_access_level > DISABLED
+ end
+
private
# Validates builds and merge requests access level
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index db46def11eb..6149c35cc61 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -16,6 +16,9 @@ class ProjectGroupLink < ActiveRecord::Base
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group
+ after_create :refresh_group_members_authorized_projects
+ after_destroy :refresh_group_members_authorized_projects
+
def self.access_options
Gitlab::Access.options
end
@@ -35,4 +38,8 @@ class ProjectGroupLink < ActiveRecord::Base
errors.add(:base, "Project cannot be shared with the project it is in.")
end
end
+
+ def refresh_group_members_authorized_projects
+ group.refresh_members_authorized_projects
+ end
end
diff --git a/app/models/project_label.rb b/app/models/project_label.rb
index 82f47f0e8fd..313815e5869 100644
--- a/app/models/project_label.rb
+++ b/app/models/project_label.rb
@@ -16,8 +16,8 @@ class ProjectLabel < Label
'project_id'
end
- def to_reference(target_project = nil, format: :id)
- super(project, target_project, format: format)
+ def to_reference(target_project = nil, format: :id, full: false)
+ super(project, target_project: target_project, format: format, full: full)
end
private
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 7c23b766763..3728f5642e4 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -25,7 +25,7 @@ You can create a Personal Access Token here:
http://app.asana.com/-/account_api'
end
- def to_param
+ def self.to_param
'asana'
end
@@ -44,7 +44,7 @@ http://app.asana.com/-/account_api'
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index d839221d315..aeeff8917bf 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -12,7 +12,7 @@ class AssemblaService < Service
'Project Management Software (Source Commits Endpoint)'
end
- def to_param
+ def self.to_param
'assembla'
end
@@ -23,7 +23,7 @@ class AssemblaService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index b5c76e4d4fe..400020ee04a 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -1,4 +1,6 @@
class BambooService < CiService
+ include ReactiveService
+
prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, presence: true, url: true, if: :activated?
@@ -38,7 +40,7 @@ class BambooService < CiService
'You must set up automatic revision labeling and a repository trigger in Bamboo.'
end
- def to_param
+ def self.to_param
'bamboo'
end
@@ -54,35 +56,46 @@ class BambooService < CiService
]
end
- def supported_events
- %w(push)
+ def build_page(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
- def build_info(sha)
- @response = get_path("rest/api/latest/result?label=#{sha}")
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
- def build_page(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ get_path("updateAndBuild.action?buildKey=#{build_key}")
+ end
+
+ def calculate_reactive_cache(sha, ref)
+ response = get_path("rest/api/latest/result?label=#{sha}")
+
+ { build_page: read_build_page(response), commit_status: read_commit_status(response) }
+ end
- if @response.code != 200 || @response['results']['results']['size'] == '0'
+ private
+
+ def read_build_page(response)
+ if response.code != 200 || response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page.
URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s
else
# If actual build link is available, go to build result page.
- result_key = @response['results']['results']['result']['planResultKey']['key']
+ result_key = response['results']['results']['result']['planResultKey']['key']
URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s
end
end
- def commit_status(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
- return :error unless @response.code == 200 || @response.code == 404
+ def read_commit_status(response)
+ return :error unless response.code == 200 || response.code == 404
- status = if @response.code == 404 || @response['results']['results']['size'] == '0'
+ status = if response.code == 404 || response['results']['results']['size'] == '0'
'Pending'
else
- @response['results']['results']['result']['buildState']
+ response['results']['results']['result']['buildState']
end
if status.include?('Success')
@@ -96,14 +109,6 @@ class BambooService < CiService
end
end
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- get_path("updateAndBuild.action?buildKey=#{build_key}")
- end
-
- private
-
def build_url(path)
URI.join("#{bamboo_url}/", path).to_s
end
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
index 338e685339a..046e2809f45 100644
--- a/app/models/project_services/bugzilla_service.rb
+++ b/app/models/project_services/bugzilla_service.rb
@@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'bugzilla'
end
end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 86a06321e21..0956c4a4ede 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -1,9 +1,12 @@
require "addressable/uri"
class BuildkiteService < CiService
+ include ReactiveService
+
ENDPOINT = "https://buildkite.com"
- prop_accessor :project_url, :token, :enable_ssl_verification
+ prop_accessor :project_url, :token
+ boolean_accessor :enable_ssl_verification
validates :project_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
@@ -21,10 +24,6 @@ class BuildkiteService < CiService
hook.save
end
- def supported_events
- %w(push)
- end
-
def execute(data)
return unless supported_events.include?(data[:object_kind])
@@ -32,13 +31,7 @@ class BuildkiteService < CiService
end
def commit_status(sha, ref)
- response = HTTParty.get(commit_status_path(sha), verify: false)
-
- if response.code == 200 && response['status']
- response['status']
- else
- :error
- end
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
def commit_status_path(sha)
@@ -57,7 +50,7 @@ class BuildkiteService < CiService
'Continuous integration and deployments'
end
- def to_param
+ def self.to_param
'buildkite'
end
@@ -77,6 +70,19 @@ class BuildkiteService < CiService
]
end
+ def calculate_reactive_cache(sha, ref)
+ response = HTTParty.get(commit_status_path(sha), verify: false)
+
+ status =
+ if response.code == 200 && response['status']
+ response['status']
+ else
+ :error
+ end
+
+ { commit_status: status }
+ end
+
private
def webhook_token
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index 201b94b065b..ebd21e37189 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -19,11 +19,11 @@ class BuildsEmailService < Service
'Email the builds status to a list of recipients.'
end
- def to_param
+ def self.to_param
'builds_email'
end
- def supported_events
+ def self.supported_events
%w(build)
end
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 5af93860d09..0de59af5652 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -12,7 +12,7 @@ class CampfireService < Service
'Simple web-based real-time group chat'
end
- def to_param
+ def self.to_param
'campfire'
end
@@ -24,7 +24,7 @@ class CampfireService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/chat_message/base_message.rb
index f1182824687..a03605d01fb 100644
--- a/app/models/project_services/slack_service/base_message.rb
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -1,6 +1,6 @@
require 'slack-notifier'
-class SlackService
+module ChatMessage
class BaseMessage
def initialize(params)
raise NotImplementedError
diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/chat_message/build_message.rb
index 0fca4267bad..53e35cb21bf 100644
--- a/app/models/project_services/slack_service/build_message.rb
+++ b/app/models/project_services/chat_message/build_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class BuildMessage < BaseMessage
attr_reader :sha
attr_reader :ref_type
diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
index cd87a79d0c6..14fd64e5332 100644
--- a/app/models/project_services/slack_service/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class IssueMessage < BaseMessage
attr_reader :user_name
attr_reader :title
diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index b7615c96068..ab5e8b24167 100644
--- a/app/models/project_services/slack_service/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class MergeMessage < BaseMessage
attr_reader :user_name
attr_reader :project_name
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/chat_message/note_message.rb
index 9e84e90f38c..ca1d7207034 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/chat_message/note_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class NoteMessage < BaseMessage
attr_reader :message
attr_reader :user_name
@@ -46,25 +46,25 @@ class SlackService
commit_sha = commit[:id]
commit_sha = Commit.truncate_sha(commit_sha)
commented_on_message(
- "[commit #{commit_sha}](#{@note_url})",
+ "commit #{commit_sha}",
format_title(commit[:message]))
end
def create_issue_note(issue)
commented_on_message(
- "[issue ##{issue[:iid]}](#{@note_url})",
+ "issue ##{issue[:iid]}",
format_title(issue[:title]))
end
def create_merge_note(merge_request)
commented_on_message(
- "[merge request !#{merge_request[:iid]}](#{@note_url})",
+ "merge request !#{merge_request[:iid]}",
format_title(merge_request[:title]))
end
def create_snippet_note(snippet)
commented_on_message(
- "[snippet ##{snippet[:id]}](#{@note_url})",
+ "snippet ##{snippet[:id]}",
format_title(snippet[:title]))
end
@@ -76,8 +76,8 @@ class SlackService
"[#{@project_name}](#{@project_url})"
end
- def commented_on_message(target_link, title)
- @message = "#{@user_name} commented on #{target_link} in #{project_link}: *#{title}*"
+ def commented_on_message(target, title)
+ @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*"
end
end
end
diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index f06b3562965..210027565a8 100644
--- a/app/models/project_services/slack_service/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -1,11 +1,10 @@
-class SlackService
+module ChatMessage
class PipelineMessage < BaseMessage
- attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url,
+ attr_reader :ref_type, :ref, :status, :project_name, :project_url,
:user_name, :duration, :pipeline_id
def initialize(data)
pipeline_attributes = data[:object_attributes]
- @sha = pipeline_attributes[:sha]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status]
@@ -14,7 +13,7 @@ class SlackService
@project_name = data[:project][:path_with_namespace]
@project_url = data[:project][:web_url]
- @user_name = data[:commit] && data[:commit][:author_name]
+ @user_name = (data[:user] && data[:user][:name]) || 'API'
end
def pretext
@@ -73,7 +72,7 @@ class SlackService
end
def pipeline_link
- "[#{Commit.truncate_sha(sha)}](#{pipeline_url})"
+ "[##{pipeline_id}](#{pipeline_url})"
end
end
end
diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/chat_message/push_message.rb
index b26f3e9ddce..2d73b71ec37 100644
--- a/app/models/project_services/slack_service/push_message.rb
+++ b/app/models/project_services/chat_message/push_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class PushMessage < BaseMessage
attr_reader :after
attr_reader :before
diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb
index 160ca3ac115..134083e4504 100644
--- a/app/models/project_services/slack_service/wiki_page_message.rb
+++ b/app/models/project_services/chat_message/wiki_page_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
class WikiPageMessage < BaseMessage
attr_reader :user_name
attr_reader :title
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
new file mode 100644
index 00000000000..8468934425f
--- /dev/null
+++ b/app/models/project_services/chat_notification_service.rb
@@ -0,0 +1,149 @@
+# Base class for Chat notifications services
+# This class is not meant to be used directly, but only to inherit from.
+class ChatNotificationService < Service
+ include ChatMessage
+
+ default_value_for :category, 'chat'
+
+ prop_accessor :webhook, :username, :channel
+ boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
+
+ validates :webhook, presence: true, url: true, if: :activated?
+
+ def initialize_properties
+ # Custom serialized properties initialization
+ self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) }
+
+ if properties.nil?
+ self.properties = {}
+ self.notify_only_broken_builds = true
+ self.notify_only_broken_pipelines = true
+ end
+ end
+
+ def can_test?
+ valid?
+ end
+
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note tag_push
+ build pipeline wiki_page]
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+ return unless webhook.present?
+
+ object_kind = data[:object_kind]
+
+ data = data.merge(
+ project_url: project_url,
+ project_name: project_name
+ )
+
+ # WebHook events often have an 'update' event that follows a 'open' or
+ # 'close' action. Ignore update events for now to prevent duplicate
+ # messages from arriving.
+
+ message = get_message(object_kind, data)
+
+ return false unless message
+
+ channel_name = get_channel_field(object_kind).presence || channel
+
+ opts = {}
+ opts[:channel] = channel_name if channel_name
+ opts[:username] = username if username
+
+ notifier = Slack::Notifier.new(webhook, opts)
+ notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
+
+ true
+ end
+
+ def event_channel_names
+ supported_events.map { |event| event_channel_name(event) }
+ end
+
+ def event_field(event)
+ fields.find { |field| field[:name] == event_channel_name(event) }
+ end
+
+ def global_fields
+ fields.reject { |field| field[:name].end_with?('channel') }
+ end
+
+ def default_channel_placeholder
+ raise NotImplementedError
+ end
+
+ private
+
+ def get_message(object_kind, data)
+ case object_kind
+ when "push", "tag_push"
+ ChatMessage::PushMessage.new(data)
+ when "issue"
+ ChatMessage::IssueMessage.new(data) unless is_update?(data)
+ when "merge_request"
+ ChatMessage::MergeMessage.new(data) unless is_update?(data)
+ when "note"
+ ChatMessage::NoteMessage.new(data)
+ when "build"
+ ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
+ when "pipeline"
+ ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+ when "wiki_page"
+ ChatMessage::WikiPageMessage.new(data)
+ end
+ end
+
+ def get_channel_field(event)
+ field_name = event_channel_name(event)
+ self.public_send(field_name)
+ end
+
+ def build_event_channels
+ supported_events.reduce([]) do |channels, event|
+ channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder }
+ end
+ end
+
+ def event_channel_name(event)
+ "#{event}_channel"
+ end
+
+ def project_name
+ project.name_with_namespace.gsub(/\s/, '')
+ end
+
+ def project_url
+ project.web_url
+ end
+
+ def is_update?(data)
+ data[:object_attributes][:action] == 'update'
+ end
+
+ def should_build_be_notified?(data)
+ case data[:commit][:status]
+ when 'success'
+ !notify_only_broken_builds?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
+
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
+ when 'success'
+ !notify_only_broken_pipelines?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
+end
diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb
new file mode 100644
index 00000000000..2bcff541cc0
--- /dev/null
+++ b/app/models/project_services/chat_slash_commands_service.rb
@@ -0,0 +1,56 @@
+# Base class for Chat services
+# This class is not meant to be used directly, but only to inherrit from.
+class ChatSlashCommandsService < Service
+ default_value_for :category, 'chat'
+
+ prop_accessor :token
+
+ has_many :chat_names, foreign_key: :service_id, dependent: :destroy
+
+ def valid_token?(token)
+ self.respond_to?(:token) &&
+ self.token.present? &&
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
+ end
+
+ def self.supported_events
+ %w()
+ end
+
+ def can_test?
+ false
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: '' }
+ ]
+ end
+
+ def trigger(params)
+ return unless valid_token?(params[:token])
+
+ user = find_chat_user(params)
+ unless user
+ url = authorize_chat_name_url(params)
+ return presenter.authorize_chat_name(url)
+ end
+
+ Gitlab::ChatCommands::Command.new(project, user,
+ params).execute
+ end
+
+ private
+
+ def find_chat_user(params)
+ ChatNames::FindUserService.new(self, params).execute
+ end
+
+ def authorize_chat_name_url(params)
+ ChatNames::AuthorizeUserService.new(self, params).execute
+ end
+
+ def presenter
+ Gitlab::ChatCommands::Presenter.new
+ end
+end
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 596c00705ad..82979c8bd34 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -8,19 +8,11 @@ class CiService < Service
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
- def supported_events
+ def self.supported_events
%w(push)
end
- def merge_request_page(iid, sha, ref)
- commit_page(sha, ref)
- end
-
- def commit_page(sha, ref)
- build_page(sha, ref)
- end
-
- # Return complete url to merge_request page
+ # Return complete url to build page
#
# Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
@@ -35,23 +27,6 @@ class CiService < Service
#
#
# Ex.
- # @service.merge_request_status(9, '13be4ac', 'dev')
- # # => 'success'
- #
- # @service.merge_request_status(10, '2abe4ac', 'dev)
- # # => 'running'
- #
- #
- def merge_request_status(iid, sha, ref)
- commit_status(sha, ref)
- end
-
- # Return string with build status or :error symbol
- #
- # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
- #
- #
- # Ex.
# @service.commit_status('13be4ac', 'master')
# # => 'success'
#
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index b2f426dc2ac..dea915a4d05 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'custom_issue_tracker'
end
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
new file mode 100644
index 00000000000..91a55514a9a
--- /dev/null
+++ b/app/models/project_services/deployment_service.rb
@@ -0,0 +1,33 @@
+# Base class for deployment services
+#
+# These services integrate with a deployment solution like Kubernetes/OpenShift,
+# Mesosphere, etc, to provide additional features to environments.
+class DeploymentService < Service
+ default_value_for :category, 'deployment'
+
+ def self.supported_events
+ %w()
+ end
+
+ def predefined_variables
+ []
+ end
+
+ # Environments may have a number of terminals. Should return an array of
+ # hashes describing them, e.g.:
+ #
+ # [{
+ # :selectors => {"a" => "b", "foo" => "bar"},
+ # :url => "wss://external.example.com/exec",
+ # :headers => {"Authorization" => "Token xxx"},
+ # :subprotocols => ["foo"],
+ # :ca_pem => "----BEGIN CERTIFICATE...", # optional
+ # :created_at => Time.now.utc
+ # }]
+ #
+ # Selectors should be a set of values that uniquely identify a particular
+ # terminal
+ def terminals(environment)
+ raise NotImplementedError
+ end
+end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 5e4dd101c53..0a217d8caba 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -1,5 +1,8 @@
class DroneCiService < CiService
- prop_accessor :drone_url, :token, :enable_ssl_verification
+ include ReactiveService
+
+ prop_accessor :drone_url, :token
+ boolean_accessor :enable_ssl_verification
validates :drone_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
@@ -29,18 +32,10 @@ class DroneCiService < CiService
true
end
- def supported_events
+ def self.supported_events
%w(push merge_request tag_push)
end
- def merge_request_status_path(iid, sha = nil, ref = nil)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
- "?access_token=#{token}"]
-
- URI.join(*url).to_s
- end
-
def commit_status_path(sha, ref)
url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
@@ -49,54 +44,34 @@ class DroneCiService < CiService
URI.join(*url).to_s
end
- def merge_request_status(iid, sha, ref)
- response = HTTParty.get(merge_request_status_path(iid), verify: enable_ssl_verification)
-
- if response.code == 200 and response['status']
- case response['status']
- when 'killed'
- :canceled
- when 'failure', 'error'
- # Because drone return error if some test env failed
- :failed
- else
- response["status"]
- end
- else
- :error
- end
- rescue Errno::ECONNREFUSED
- :error
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
- def commit_status(sha, ref)
+ def calculate_reactive_cache(sha, ref)
response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
- if response.code == 200 and response['status']
- case response['status']
- when 'killed'
- :canceled
- when 'failure', 'error'
- # Because drone return error if some test env failed
- :failed
+ status =
+ if response.code == 200 and response['status']
+ case response['status']
+ when 'killed'
+ :canceled
+ when 'failure', 'error'
+ # Because drone return error if some test env failed
+ :failed
+ else
+ response["status"]
+ end
else
- response["status"]
+ :error
end
- else
- :error
- end
- rescue Errno::ECONNREFUSED
- :error
- end
- def merge_request_page(iid, sha, ref)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"]
-
- URI.join(*url).to_s
+ { commit_status: status }
+ rescue Errno::ECONNREFUSED
+ { commit_status: :error }
end
- def commit_page(sha, ref)
+ def build_page(sha, ref)
url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}"]
@@ -104,14 +79,6 @@ class DroneCiService < CiService
URI.join(*url).to_s
end
- def commit_coverage(sha, ref)
- nil
- end
-
- def build_page(sha, ref)
- commit_page(sha, ref)
- end
-
def title
'Drone CI'
end
@@ -120,7 +87,7 @@ class DroneCiService < CiService
'Drone is a Continuous Integration platform built on Docker, written in Go'
end
- def to_param
+ def self.to_param
'drone_ci'
end
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index e0083c43adb..f4f913ee0b6 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -1,6 +1,6 @@
class EmailsOnPushService < Service
- prop_accessor :send_from_committer_email
- prop_accessor :disable_diffs
+ boolean_accessor :send_from_committer_email
+ boolean_accessor :disable_diffs
prop_accessor :recipients
validates :recipients, presence: true, if: :activated?
@@ -12,11 +12,11 @@ class EmailsOnPushService < Service
'Email the commits and diff of each push to a list of recipients.'
end
- def to_param
+ def self.to_param
'emails_on_push'
end
- def supported_events
+ def self.supported_events
%w(push tag_push)
end
@@ -24,20 +24,20 @@ class EmailsOnPushService < Service
return unless supported_events.include?(push_data[:object_kind])
EmailsOnPushWorker.perform_async(
- project_id,
- recipients,
- push_data,
- send_from_committer_email: send_from_committer_email?,
- disable_diffs: disable_diffs?
+ project_id,
+ recipients,
+ push_data,
+ send_from_committer_email: send_from_committer_email?,
+ disable_diffs: disable_diffs?
)
end
def send_from_committer_email?
- self.send_from_committer_email == "1"
+ Gitlab::Utils.to_boolean(self.send_from_committer_email)
end
def disable_diffs?
- self.disable_diffs == "1"
+ Gitlab::Utils.to_boolean(self.disable_diffs)
end
def fields
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index d7b6e505191..bdf6fa6a586 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -13,7 +13,7 @@ class ExternalWikiService < Service
'Replaces the link to the internal wiki with a link to an external wiki.'
end
- def to_param
+ def self.to_param
'external_wiki'
end
@@ -29,4 +29,8 @@ class ExternalWikiService < Service
nil
end
end
+
+ def self.supported_events
+ %w()
+ end
end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index dd00275187f..10a13c3fbdc 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -12,7 +12,7 @@ class FlowdockService < Service
'Flowdock is a collaboration web app for technical teams.'
end
- def to_param
+ def self.to_param
'flowdock'
end
@@ -22,7 +22,7 @@ class FlowdockService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 598aca5e06d..f271e1f1739 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -12,7 +12,7 @@ class GemnasiumService < Service
'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.'
end
- def to_param
+ def self.to_param
'gemnasium'
end
@@ -23,7 +23,7 @@ class GemnasiumService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 6bd8d4ec568..ad4eb9536e1 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService
default_value_for :default, true
- def to_param
+ def self.to_param
'gitlab'
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 660a8ae3421..72da219df28 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -8,8 +8,8 @@ class HipchatService < Service
ul ol li dl dt dd
]
- prop_accessor :token, :room, :server, :notify, :color, :api_version
- boolean_accessor :notify_only_broken_builds
+ prop_accessor :token, :room, :server, :color, :api_version
+ boolean_accessor :notify_only_broken_builds, :notify
validates :token, presence: true, if: :activated?
def initialize_properties
@@ -27,7 +27,7 @@ class HipchatService < Service
'Private group chat and IM'
end
- def to_param
+ def self.to_param
'hipchat'
end
@@ -45,7 +45,7 @@ class HipchatService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push issue confidential_issue merge_request note tag_push build)
end
@@ -75,7 +75,7 @@ class HipchatService < Service
end
def message_options(data = nil)
- { notify: notify.present? && notify == '1', color: message_color(data) }
+ { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
end
def create_message(data)
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index ce7d1c5d5b1..5d93064f9b3 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -2,7 +2,8 @@ require 'uri'
class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri
- prop_accessor :colorize_messages, :recipients, :channels
+ prop_accessor :recipients, :channels
+ boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :activated?
before_validation :get_channels
@@ -16,11 +17,11 @@ class IrkerService < Service
'gateway.'
end
- def to_param
+ def self.to_param
'irker'
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 207bb816ad1..9e65fdbf9d6 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -57,7 +57,7 @@ class IssueTrackerService < Service
end
end
- def supported_events
+ def self.supported_events
%w(push)
end
@@ -85,8 +85,8 @@ class IssueTrackerService < Service
def enabled_in_gitlab_config
Gitlab.config.issues_tracker &&
- Gitlab.config.issues_tracker.values.any? &&
- issues_tracker
+ Gitlab.config.issues_tracker.values.any? &&
+ issues_tracker
end
def issues_tracker
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2dbe0075465..2ac76e97de0 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,24 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-# build_events :boolean default(FALSE), not null
-#
-
class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers
@@ -30,6 +9,13 @@ class JiraService < IssueTrackerService
before_update :reset_password
+ # This is confusing, but JiraService does not really support these events.
+ # The values here are required to display correct options in the service
+ # configuration screen.
+ def self.supported_events
+ %w(commit merge_request)
+ end
+
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
@@ -70,13 +56,13 @@ class JiraService < IssueTrackerService
end
def jira_project
- @jira_project ||= client.Project.find(project_key)
+ @jira_project ||= jira_request { client.Project.find(project_key) }
end
def help
- 'See the ' \
- '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\
- 'for details.'
+ 'You need to configure JIRA before enabling this service. For more details
+ read the
+ [JIRA service documentation](https://docs.gitlab.com/ce/project_services/jira.html).'
end
def title
@@ -95,7 +81,7 @@ class JiraService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'jira'
end
@@ -122,27 +108,43 @@ class JiraService < IssueTrackerService
"#{url}/secure/CreateIssue.jspa"
end
- def execute(push, issue = nil)
- if issue.nil?
- # No specific issue, that means
- # we just want to test settings
- test_settings
- else
- close_issue(push, issue)
- end
+ def execute(push)
+ # This method is a no-op, because currently JiraService does not
+ # support any events.
+ end
+
+ def close_issue(entity, external_issue)
+ issue = jira_request { client.Issue.find(external_issue.iid) }
+
+ return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present?
+
+ commit_id = if entity.is_a?(Commit)
+ entity.id
+ elsif entity.is_a?(MergeRequest)
+ entity.diff_head_sha
+ end
+
+ commit_url = build_entity_url(:commit, commit_id)
+
+ # Depending on the JIRA project's workflow, a comment during transition
+ # may or may not be allowed. Refresh the issue after transition and check
+ # if it is closed, so we don't have one comment for every commit.
+ issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue)
+ add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution
end
def create_cross_reference_note(mentioned, noteable, author)
- issue_key = mentioned.id
- project = self.project
- noteable_name = noteable.class.name.underscore.downcase
- noteable_id = if noteable.is_a?(Commit)
- noteable.id
- else
- noteable.iid
- end
+ unless can_cross_reference?(noteable)
+ return "Events for #{noteable.model_name.plural.humanize(capitalize: false)} are disabled."
+ end
+
+ jira_issue = jira_request { client.Issue.find(mentioned.id) }
+
+ return unless jira_issue.present?
- entity_url = build_entity_url(noteable_name.to_sym, noteable_id)
+ noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
+ noteable_type = noteable_name(noteable)
+ entity_url = build_entity_url(noteable_type, noteable_id)
data = {
user: {
@@ -150,17 +152,17 @@ class JiraService < IssueTrackerService
url: resource_url(user_path(author)),
},
project: {
- name: project.path_with_namespace,
- url: resource_url(namespace_project_path(project.namespace, project))
+ name: self.project.path_with_namespace,
+ url: resource_url(namespace_project_path(project.namespace, self.project))
},
entity: {
- name: noteable_name.humanize.downcase,
+ name: noteable_type.humanize.downcase,
url: entity_url,
title: noteable.title
}
}
- add_comment(data, issue_key)
+ add_comment(data, jira_issue)
end
# reason why service cannot be tested
@@ -168,6 +170,11 @@ class JiraService < IssueTrackerService
"Please fill in Password and Username."
end
+ def test(_)
+ result = test_settings
+ { success: result.present?, result: result }
+ end
+
def can_test?
username.present? && password.present?
end
@@ -181,91 +188,124 @@ class JiraService < IssueTrackerService
def test_settings
return unless url.present?
# Test settings by getting the project
- jira_project
-
- rescue Errno::ECONNREFUSED, JIRA::HTTPError => e
- Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}."
- false
+ jira_request { jira_project.present? }
end
private
- def close_issue(entity, issue)
- commit_id = if entity.is_a?(Commit)
- entity.id
- elsif entity.is_a?(MergeRequest)
- entity.diff_head_sha
- end
-
- commit_url = build_entity_url(:commit, commit_id)
-
- # Depending on the JIRA project's workflow, a comment during transition
- # may or may not be allowed. Split the operation in to two calls so the
- # comment always works.
- transition_issue(issue)
- add_issue_solved_comment(issue, commit_id, commit_url)
+ def can_cross_reference?(noteable)
+ case noteable
+ when Commit then commit_events
+ when MergeRequest then merge_requests_events
+ else true
+ end
end
def transition_issue(issue)
- issue = client.Issue.find(issue.iid)
issue.transitions.build.save(transition: { id: jira_issue_transition_id })
end
def add_issue_solved_comment(issue, commit_id, commit_url)
- comment = "Issue solved with [#{commit_id}|#{commit_url}]."
- send_message(issue.iid, comment)
+ link_title = "GitLab: Solved by commit #{commit_id}."
+ comment = "Issue solved with [#{commit_id}|#{commit_url}]."
+ link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true)
+ send_message(issue, comment, link_props)
end
- def add_comment(data, issue_key)
- user_name = data[:user][:name]
- user_url = data[:user][:url]
- entity_name = data[:entity][:name]
- entity_url = data[:entity][:url]
+ def add_comment(data, issue)
+ user_name = data[:user][:name]
+ user_url = data[:user][:url]
+ entity_name = data[:entity][:name]
+ entity_url = data[:entity][:url]
entity_title = data[:entity][:title]
project_name = data[:project][:name]
- message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
+ message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'"
+ link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
+ link_props = build_remote_link_props(url: entity_url, title: link_title)
- unless comment_exists?(issue_key, message)
- send_message(issue_key, message)
+ unless comment_exists?(issue, message)
+ send_message(issue, message, link_props)
end
end
- def comment_exists?(issue_key, message)
- comments = client.Issue.find(issue_key).comments
- comments.map { |comment| comment.body.include?(message) }.any?
+ def comment_exists?(issue, message)
+ comments = jira_request { issue.comments }
+
+ comments.present? && comments.any? { |comment| comment.body.include?(message) }
end
- def send_message(issue_key, message)
+ def send_message(issue, message, remote_link_props)
return unless url.present?
- issue = client.Issue.find(issue_key)
+ jira_request do
+ if issue.comments.build.save!(body: message)
+ remote_link = issue.remotelink.build
+ remote_link.save!(remote_link_props)
+ result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ end
- if issue.comments.build.save!(body: message)
- result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ Rails.logger.info(result_message)
+ result_message
end
+ end
- Rails.logger.info(result_message)
- result_message
- rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e
- Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ # Build remote link on JIRA properties
+ # Icons here must be available on WEB so JIRA can read the URL
+ # We are using a open word graphics icon which have LGPL license
+ def build_remote_link_props(url:, title:, resolved: false)
+ status = {
+ resolved: resolved
+ }
+
+ if resolved
+ status[:icon] = {
+ title: 'Closed',
+ url16x16: 'http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png'
+ }
+ end
+
+ {
+ GlobalID: 'GitLab',
+ object: {
+ url: url,
+ title: title,
+ status: status,
+ icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' }
+ }
+ }
end
def resource_url(resource)
"#{Settings.gitlab.base_url.chomp("/")}#{resource}"
end
- def build_entity_url(entity_name, entity_id)
- resource_url(
- polymorphic_url(
- [
- self.project.namespace.becomes(Namespace),
- self.project,
- entity_name
- ],
- id: entity_id,
- routing_type: :path
- )
+ def build_entity_url(noteable_type, entity_id)
+ polymorphic_url(
+ [
+ self.project.namespace.becomes(Namespace),
+ self.project,
+ noteable_type.to_sym
+ ],
+ id: entity_id,
+ host: Settings.gitlab.base_url
)
end
+
+ def noteable_name(noteable)
+ name = noteable.model_name.singular
+
+ # ProjectSnippet inherits from Snippet class so it causes
+ # routing error building the URL.
+ name == "project_snippet" ? "snippet" : name
+ end
+
+ # Handle errors when doing JIRA API calls
+ def jira_request
+ yield
+
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError => e
+ Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ nil
+ end
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
new file mode 100644
index 00000000000..fa3cedc4354
--- /dev/null
+++ b/app/models/project_services/kubernetes_service.rb
@@ -0,0 +1,173 @@
+class KubernetesService < DeploymentService
+ include Gitlab::Kubernetes
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+
+ # Namespace defaults to the project path, but can be overridden in case that
+ # is an invalid or inappropriate name
+ prop_accessor :namespace
+
+ # Access to kubernetes is directly through the API
+ prop_accessor :api_url
+
+ # Bearer authentication
+ # TODO: user/password auth, client certificates
+ prop_accessor :token
+
+ # Provide a custom CA bundle for self-signed deployments
+ prop_accessor :ca_pem
+
+ with_options presence: true, if: :activated? do
+ validates :api_url, url: true
+ validates :token
+
+ validates :namespace,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message,
+ },
+ length: 1..63
+ end
+
+ after_save :clear_reactive_cache!
+
+ def initialize_properties
+ if properties.nil?
+ self.properties = {}
+ self.namespace = project.path if project.present?
+ end
+ end
+
+ def title
+ 'Kubernetes'
+ end
+
+ def description
+ 'Kubernetes / Openshift integration'
+ end
+
+ def help
+ 'To enable terminal access to Kubernetes environments, label your ' \
+ 'deployments with `app=$CI_ENVIRONMENT_SLUG`'
+ end
+
+ def self.to_param
+ 'kubernetes'
+ end
+
+ def fields
+ [
+ { type: 'text',
+ name: 'namespace',
+ title: 'Kubernetes namespace',
+ placeholder: 'Kubernetes namespace',
+ },
+ { type: 'text',
+ name: 'api_url',
+ title: 'API URL',
+ placeholder: 'Kubernetes API URL, like https://kube.example.com/',
+ },
+ { type: 'text',
+ name: 'token',
+ title: 'Service token',
+ placeholder: 'Service token',
+ },
+ { type: 'textarea',
+ name: 'ca_pem',
+ title: 'Custom CA bundle',
+ placeholder: 'Certificate Authority bundle (PEM format)',
+ },
+ ]
+ end
+
+ # Check we can connect to the Kubernetes API
+ def test(*args)
+ kubeclient = build_kubeclient!
+
+ kubeclient.discover
+ { success: kubeclient.discovered, result: "Checked API discovery endpoint" }
+ rescue => err
+ { success: false, result: err }
+ end
+
+ def predefined_variables
+ variables = [
+ { key: 'KUBE_URL', value: api_url, public: true },
+ { key: 'KUBE_TOKEN', value: token, public: false },
+ { key: 'KUBE_NAMESPACE', value: namespace, public: true }
+ ]
+ variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present?
+ variables
+ end
+
+ # Constructs a list of terminals from the reactive cache
+ #
+ # Returns nil if the cache is empty, in which case you should try again a
+ # short time later
+ def terminals(environment)
+ with_reactive_cache do |data|
+ pods = data.fetch(:pods, nil)
+ filter_pods(pods, app: environment.slug).
+ flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
+ map { |terminal| add_terminal_auth(terminal, token, ca_pem) }
+ end
+ end
+
+ # Caches all pods in the namespace so other calls don't need to block on
+ # network access.
+ def calculate_reactive_cache
+ return unless active? && project && !project.pending_delete?
+
+ kubeclient = build_kubeclient!
+
+ # Store as hashes, rather than as third-party types
+ pods = begin
+ kubeclient.get_pods(namespace: namespace).as_json
+ rescue KubeException => err
+ raise err unless err.error_code == 404
+ []
+ end
+
+ # We may want to cache extra things in the future
+ { pods: pods }
+ end
+
+ private
+
+ def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ raise "Incomplete settings" unless api_url && namespace && token
+
+ ::Kubeclient::Client.new(
+ join_api_url(api_path),
+ api_version,
+ auth_options: kubeclient_auth_options,
+ ssl_options: kubeclient_ssl_options,
+ http_proxy_uri: ENV['http_proxy']
+ )
+ end
+
+ def kubeclient_ssl_options
+ opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
+
+ if ca_pem.present?
+ opts[:cert_store] = OpenSSL::X509::Store.new
+ opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+ end
+
+ opts
+ end
+
+ def kubeclient_auth_options
+ { bearer_token: token }
+ end
+
+ def join_api_url(*parts)
+ url = URI.parse(api_url)
+ prefix = url.path.sub(%r{/+\z}, '')
+
+ url.path = [ prefix, *parts ].join("/")
+
+ url.to_s
+ end
+end
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
new file mode 100644
index 00000000000..4ebc5318da1
--- /dev/null
+++ b/app/models/project_services/mattermost_service.rb
@@ -0,0 +1,41 @@
+class MattermostService < ChatNotificationService
+ def title
+ 'Mattermost notifications'
+ end
+
+ def description
+ 'Receive event notifications in Mattermost'
+ end
+
+ def self.to_param
+ 'mattermost'
+ end
+
+ def help
+ 'This service sends notifications about projects events to Mattermost channels.<br />
+ To set up this service:
+ <ol>
+ <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li>
+ <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li>
+ <li>Paste the webhook <strong>URL</strong> into the field bellow. </li>
+ <li>Select events below to enable notifications. The channel and username are optional. </li>
+ </ol>'
+ end
+
+ def fields
+ default_fields + build_event_channels
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
+ { type: 'text', name: 'username', placeholder: 'username' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ ]
+ end
+
+ def default_channel_placeholder
+ "town-square"
+ end
+end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
new file mode 100644
index 00000000000..50a011db74e
--- /dev/null
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -0,0 +1,51 @@
+class MattermostSlashCommandsService < ChatSlashCommandsService
+ include TriggersHelper
+
+ prop_accessor :token
+
+ def can_test?
+ false
+ end
+
+ def title
+ 'Mattermost Command'
+ end
+
+ def description
+ "Perform common operations on GitLab in Mattermost"
+ end
+
+ def self.to_param
+ 'mattermost_slash_commands'
+ end
+
+ def configure(user, params)
+ token = Mattermost::Command.new(user).
+ create(command(params))
+
+ update(active: true, token: token) if token
+ rescue Mattermost::Error => e
+ [false, e.message]
+ end
+
+ def list_teams(user)
+ Mattermost::Team.new(user).all
+ rescue Mattermost::Error => e
+ [[], e.message]
+ end
+
+ private
+
+ def command(params)
+ pretty_project_name = project.name_with_namespace
+
+ params.merge(
+ auto_complete: true,
+ auto_complete_desc: "Perform common operations on: #{pretty_project_name}",
+ auto_complete_hint: '[help]',
+ description: "Perform common operations on: #{pretty_project_name}",
+ display_name: "GitLab / #{pretty_project_name}",
+ method: 'P',
+ username: 'GitLab')
+ end
+end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 745f9bd1b43..ac617f409d9 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -15,11 +15,11 @@ class PipelinesEmailService < Service
'Email the pipelines status to a list of recipients.'
end
- def to_param
+ def self.to_param
'pipelines_email'
end
- def supported_events
+ def self.supported_events
%w[pipeline]
end
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index 5301f9fa0ff..9cc642591f4 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -14,7 +14,7 @@ class PivotaltrackerService < Service
'Project Management Software (Source Commits Endpoint)'
end
- def to_param
+ def self.to_param
'pivotaltracker'
end
@@ -34,7 +34,7 @@ class PivotaltrackerService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 3dd878e4c7d..a963d27a376 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -13,7 +13,7 @@ class PushoverService < Service
'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.'
end
- def to_param
+ def self.to_param
'pushover'
end
@@ -61,7 +61,7 @@ class PushoverService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index f9da273cf08..6acf611eba5 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'redmine'
end
end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index e1b937817f4..f77d2d7c60b 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -1,176 +1,40 @@
-class SlackService < Service
- prop_accessor :webhook, :username, :channel
- boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
- validates :webhook, presence: true, url: true, if: :activated?
-
- def initialize_properties
- # Custom serialized properties initialization
- self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) }
-
- if properties.nil?
- self.properties = {}
- self.notify_only_broken_builds = true
- self.notify_only_broken_pipelines = true
- end
- end
-
+class SlackService < ChatNotificationService
def title
- 'Slack'
+ 'Slack notifications'
end
def description
- 'A team communication tool for the 21st century'
+ 'Receive event notifications in Slack'
end
- def to_param
+ def self.to_param
'slack'
end
def help
- 'This service sends notifications to your Slack channel.<br/>
- To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel,
- and enter the Webhook URL below.'
+ 'This service sends notifications about projects events to Slack channels.<br />
+ To setup this service:
+ <ol>
+ <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li>
+ <li>Paste the <strong>Webhook URL</strong> into the field below. </li>
+ <li>Select events below to enable notifications. The channel and username are optional. </li>
+ </ol>'
end
def fields
- default_fields =
- [
- { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
- { type: 'text', name: 'channel', placeholder: "#general" },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' },
- ]
-
default_fields + build_event_channels
end
- def supported_events
- %w[push issue confidential_issue merge_request note tag_push
- build pipeline wiki_page]
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
- return unless webhook.present?
-
- object_kind = data[:object_kind]
-
- data = data.merge(
- project_url: project_url,
- project_name: project_name
- )
-
- # WebHook events often have an 'update' event that follows a 'open' or
- # 'close' action. Ignore update events for now to prevent duplicate
- # messages from arriving.
-
- message = get_message(object_kind, data)
-
- if message
- opt = {}
-
- event_channel = get_channel_field(object_kind) || channel
-
- opt[:channel] = event_channel if event_channel
- opt[:username] = username if username
-
- notifier = Slack::Notifier.new(webhook, opt)
- notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
-
- true
- else
- false
- end
- end
-
- def event_channel_names
- supported_events.map { |event| event_channel_name(event) }
- end
-
- def event_field(event)
- fields.find { |field| field[:name] == event_channel_name(event) }
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
+ { type: 'text', name: 'username', placeholder: 'username' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ ]
end
- def global_fields
- fields.reject { |field| field[:name].end_with?('channel') }
- end
-
- private
-
- def get_message(object_kind, data)
- case object_kind
- when "push", "tag_push"
- PushMessage.new(data)
- when "issue"
- IssueMessage.new(data) unless is_update?(data)
- when "merge_request"
- MergeMessage.new(data) unless is_update?(data)
- when "note"
- NoteMessage.new(data)
- when "build"
- BuildMessage.new(data) if should_build_be_notified?(data)
- when "pipeline"
- PipelineMessage.new(data) if should_pipeline_be_notified?(data)
- when "wiki_page"
- WikiPageMessage.new(data)
- end
- end
-
- def get_channel_field(event)
- field_name = event_channel_name(event)
- self.public_send(field_name)
- end
-
- def build_event_channels
- supported_events.reduce([]) do |channels, event|
- channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" }
- end
- end
-
- def event_channel_name(event)
- "#{event}_channel"
- end
-
- def project_name
- project.name_with_namespace.gsub(/\s/, '')
- end
-
- def project_url
- project.web_url
- end
-
- def is_update?(data)
- data[:object_attributes][:action] == 'update'
- end
-
- def should_build_be_notified?(data)
- case data[:commit][:status]
- when 'success'
- !notify_only_broken_builds?
- when 'failed'
- true
- else
- false
- end
- end
-
- def should_pipeline_be_notified?(data)
- case data[:object_attributes][:status]
- when 'success'
- !notify_only_broken_pipelines?
- when 'failed'
- true
- else
- false
- end
+ def default_channel_placeholder
+ "#general"
end
end
-
-require "slack_service/issue_message"
-require "slack_service/push_message"
-require "slack_service/merge_message"
-require "slack_service/note_message"
-require "slack_service/build_message"
-require "slack_service/pipeline_message"
-require "slack_service/wiki_page_message"
diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
new file mode 100644
index 00000000000..c34991e4262
--- /dev/null
+++ b/app/models/project_services/slack_slash_commands_service.rb
@@ -0,0 +1,28 @@
+class SlackSlashCommandsService < ChatSlashCommandsService
+ include TriggersHelper
+
+ def title
+ 'Slack Command'
+ end
+
+ def description
+ "Perform common operations on GitLab in Slack"
+ end
+
+ def self.to_param
+ 'slack_slash_commands'
+ end
+
+ def trigger(params)
+ # Format messages to be Slack-compatible
+ super.tap do |result|
+ result[:text] = format(result[:text]) if result.is_a?(Hash)
+ end
+ end
+
+ private
+
+ def format(text)
+ Slack::Notifier::LinkFormatter.format(text) if text
+ end
+end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index a4a967c9bc9..cbaffb8ce48 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -1,4 +1,6 @@
class TeamcityService < CiService
+ include ReactiveService
+
prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, presence: true, url: true, if: :activated?
@@ -41,14 +43,10 @@ class TeamcityService < CiService
'requests build, that setting is in the vsc root advanced settings.'
end
- def to_param
+ def self.to_param
'teamcity'
end
- def supported_events
- %w(push)
- end
-
def fields
[
{ type: 'text', name: 'teamcity_url',
@@ -61,43 +59,18 @@ class TeamcityService < CiService
]
end
- def build_info(sha)
- @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
- end
-
def build_page(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
-
- if @response.code != 200
- # If actual build link can't be determined,
- # send user to build summary page.
- build_url("viewLog.html?buildTypeId=#{build_type}")
- else
- # If actual build link is available, go to build result page.
- built_id = @response['build']['id']
- build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
- end
+ with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
def commit_status(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
- return :error unless @response.code == 200 || @response.code == 404
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
+ end
- status = if @response.code == 404
- 'Pending'
- else
- @response['build']['status']
- end
+ def calculate_reactive_cache(sha, ref)
+ response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
- if status.include?('SUCCESS')
- 'success'
- elsif status.include?('FAILURE')
- 'failed'
- elsif status.include?('Pending')
- 'pending'
- else
- :error
- end
+ { build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
def execute(data)
@@ -122,6 +95,40 @@ class TeamcityService < CiService
private
+ def read_build_page(response)
+ if response.code != 200
+ # If actual build link can't be determined,
+ # send user to build summary page.
+ build_url("viewLog.html?buildTypeId=#{build_type}")
+ else
+ # If actual build link is available, go to build result page.
+ built_id = response['build']['id']
+ build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
+ end
+ end
+
+ def read_commit_status(response)
+ return :error unless response.code == 200 || response.code == 404
+
+ status = if response.code == 404
+ 'Pending'
+ else
+ response['build']['status']
+ end
+
+ return :error unless status.present?
+
+ if status.include?('SUCCESS')
+ 'success'
+ elsif status.include?('FAILURE')
+ 'failed'
+ elsif status.include?('Pending')
+ 'pending'
+ else
+ :error
+ end
+ end
+
def build_url(path)
URI.join("#{teamcity_url}/", path).to_s
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
new file mode 100644
index 00000000000..06abd406523
--- /dev/null
+++ b/app/models/project_statistics.rb
@@ -0,0 +1,44 @@
+class ProjectStatistics < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :namespace
+
+ before_save :update_storage_size
+
+ STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size]
+ STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
+
+ def total_repository_size
+ repository_size + lfs_objects_size
+ end
+
+ def refresh!(only: nil)
+ STATISTICS_COLUMNS.each do |column, generator|
+ if only.blank? || only.include?(column)
+ public_send("update_#{column}")
+ end
+ end
+
+ save!
+ end
+
+ def update_commit_count
+ self.commit_count = project.repository.commit_count
+ end
+
+ # Repository#size needs to be converted from MB to Byte.
+ def update_repository_size
+ self.repository_size = project.repository.size * 1.megabyte
+ end
+
+ def update_lfs_objects_size
+ self.lfs_objects_size = project.lfs_objects.sum(:size)
+ end
+
+ def update_build_artifacts_size
+ self.build_artifacts_size = project.builds.sum(:artifacts_size)
+ end
+
+ def update_storage_size
+ self.storage_size = STORAGE_COLUMNS.sum(&method(:read_attribute))
+ end
+end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index a6e911df9bd..8a53e974b6f 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -21,6 +21,22 @@ class ProjectTeam
end
end
+ def add_guest(user, current_user: nil)
+ self << [user, :guest, current_user]
+ end
+
+ def add_reporter(user, current_user: nil)
+ self << [user, :reporter, current_user]
+ end
+
+ def add_developer(user, current_user: nil)
+ self << [user, :developer, current_user]
+ end
+
+ def add_master(user, current_user: nil)
+ self << [user, :master, current_user]
+ end
+
def find_member(user_id)
member = project.members.find_by(user_id: user_id)
@@ -64,19 +80,19 @@ class ProjectTeam
alias_method :users, :members
def guests
- @guests ||= fetch_members(:guests)
+ @guests ||= fetch_members(Gitlab::Access::GUEST)
end
def reporters
- @reporters ||= fetch_members(:reporters)
+ @reporters ||= fetch_members(Gitlab::Access::REPORTER)
end
def developers
- @developers ||= fetch_members(:developers)
+ @developers ||= fetch_members(Gitlab::Access::DEVELOPER)
end
def masters
- @masters ||= fetch_members(:masters)
+ @masters ||= fetch_members(Gitlab::Access::MASTER)
end
def import(source_project, current_user = nil)
@@ -125,8 +141,12 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::MASTER
end
- def member?(user, min_member_access = Gitlab::Access::GUEST)
- max_member_access(user.id) >= min_member_access
+ # Checks if `user` is authorized for this project, with at least the
+ # `min_access_level` (if given).
+ def member?(user, min_access_level = Gitlab::Access::GUEST)
+ return false unless user
+
+ user.authorized_project?(project, min_access_level)
end
def human_max_access(user_id)
@@ -149,112 +169,29 @@ class ProjectTeam
# Lookup only the IDs we need
user_ids = user_ids - access.keys
+ users_access = project.project_authorizations.
+ where(user: user_ids).
+ group(:user_id).
+ maximum(:access_level)
- if user_ids.present?
- user_ids.each { |id| access[id] = Gitlab::Access::NO_ACCESS }
-
- member_access = project.members.access_for_user_ids(user_ids)
- merge_max!(access, member_access)
-
- if group
- group_access = group.members.access_for_user_ids(user_ids)
- merge_max!(access, group_access)
- end
-
- # Each group produces a list of maximum access level per user. We take the
- # max of the values produced by each group.
- if project_shared_with_group?
- project.project_group_links.each do |group_link|
- invited_access = max_invited_level_for_users(group_link, user_ids)
- merge_max!(access, invited_access)
- end
- end
- end
-
+ access.merge!(users_access)
access
end
def max_member_access(user_id)
- max_member_access_for_user_ids([user_id])[user_id]
+ max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS
end
private
- # For a given group, return the maximum access level for the user. This is the min of
- # the invited access level of the group and the access level of the user within the group.
- # For example, if the group has been given DEVELOPER access but the member has MASTER access,
- # the user should receive only DEVELOPER access.
- def max_invited_level_for_users(group_link, user_ids)
- invited_group = group_link.group
- capped_access_level = group_link.group_access
- access = invited_group.group_members.access_for_user_ids(user_ids)
-
- # If the user is not in the list, assume he/she does not have access
- missing_users = user_ids - access.keys
- missing_users.each { |id| access[id] = Gitlab::Access::NO_ACCESS }
-
- # Cap the maximum access by the invited level access
- access.each { |key, value| access[key] = [value, capped_access_level].min }
- end
-
def fetch_members(level = nil)
- project_members = project.members
- group_members = group ? group.members : []
-
- if level
- project_members = project_members.public_send(level)
- group_members = group_members.public_send(level) if group
- end
-
- user_ids = project_members.pluck(:user_id)
-
- invited_members = fetch_invited_members(level)
- user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
+ members = project.authorized_users
+ members = members.where(project_authorizations: { access_level: level }) if level
- user_ids.push(*group_members.pluck(:user_id)) if group
-
- User.where(id: user_ids)
+ members
end
def group
project.group
end
-
- def merge_max!(first_hash, second_hash)
- first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new }
- end
-
- def project_shared_with_group?
- project.invited_groups.any? && project.allowed_to_share_with_group?
- end
-
- def fetch_invited_members(level = nil)
- invited_members = []
-
- return invited_members unless project_shared_with_group?
-
- project.project_group_links.includes(group: [:group_members]).each do |link|
- invited_group_members = link.group.members
-
- if level
- numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
-
- # If we're asked for a level that's higher than the group's access,
- # there's nothing left to do
- next if numeric_level > link.group_access
-
- # Make sure we include everyone _above_ the requested level as well
- invited_group_members =
- if numeric_level == link.group_access
- invited_group_members.where("access_level >= ?", link.group_access)
- else
- invited_group_members.public_send(level)
- end
- end
-
- invited_members << invited_group_members
- end
-
- invited_members.flatten.compact
- end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 46f70da2452..9db96347322 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -127,7 +127,7 @@ class ProjectWiki
end
def search_files(query)
- repository.search_files(query, default_branch)
+ repository.search_files_by_content(query, default_branch)
end
def repository
diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb
index 806b3ccd275..771e3376613 100644
--- a/app/models/protected_branch/merge_access_level.rb
+++ b/app/models/protected_branch/merge_access_level.rb
@@ -1,9 +1,6 @@
class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess
- belongs_to :protected_branch
- delegate :project, to: :protected_branch
-
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER] }
@@ -13,10 +10,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
Gitlab::Access::DEVELOPER => "Developers + Masters"
}.with_indifferent_access
end
-
- def check_access(user)
- return true if user.is_admin?
-
- project.team.max_member_access(user.id) >= access_level
- end
end
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
index 92e9c51d883..14610cb42b7 100644
--- a/app/models/protected_branch/push_access_level.rb
+++ b/app/models/protected_branch/push_access_level.rb
@@ -1,9 +1,6 @@
class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess
- belongs_to :protected_branch
- delegate :project, to: :protected_branch
-
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER,
Gitlab::Access::NO_ACCESS] }
@@ -18,8 +15,7 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
- return true if user.is_admin?
- project.team.max_member_access(user.id) >= access_level
+ super
end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 7d06ce1e85b..43dba86e5ed 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1,28 +1,56 @@
require 'securerandom'
class Repository
- class CommitError < StandardError; end
-
- # Files to use as a project avatar in case no avatar was uploaded via the web
- # UI.
- AVATAR_FILES = %w{logo.png logo.jpg logo.gif}
-
include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project
- def self.storages
- Gitlab.config.repositories.storages
- end
+ class CommitError < StandardError; end
- def self.remove_storage_from_path(repo_path)
- storages.find do |_, storage_path|
- if repo_path.start_with?(storage_path)
- return repo_path.sub(storage_path, '')
- end
+ # Methods that cache data from the Git repository.
+ #
+ # Each entry in this Array should have a corresponding method with the exact
+ # same name. The cache key used by those methods must also match method's
+ # name.
+ #
+ # For example, for entry `:readme` there's a method called `readme` which
+ # stores its data in the `readme` cache key.
+ CACHED_METHODS = %i(size commit_count readme version contribution_guide
+ changelog license_blob license_key gitignore koding_yml
+ gitlab_ci_yml branch_names tag_names branch_count
+ tag_count avatar exists? empty? root_ref)
+
+ # Certain method caches should be refreshed when certain types of files are
+ # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
+ # the corresponding methods to call for refreshing caches.
+ METHOD_CACHES_FOR_FILE_TYPES = {
+ readme: :readme,
+ changelog: :changelog,
+ license: %i(license_blob license_key),
+ contributing: :contribution_guide,
+ version: :version,
+ gitignore: :gitignore,
+ koding: :koding_yml,
+ gitlab_ci: :gitlab_ci_yml,
+ avatar: :avatar
+ }
+
+ # Wraps around the given method and caches its output in Redis and an instance
+ # variable.
+ #
+ # This only works for methods that do not take any arguments.
+ def self.cache_method(name, fallback: nil)
+ original = :"_uncached_#{name}"
+
+ alias_method(original, name)
+
+ define_method(name) do
+ cache_method_output(name, fallback: fallback) { __send__(original) }
end
+ end
- repo_path
+ def self.storages
+ Gitlab.config.repositories.storages
end
def initialize(path_with_namespace, project)
@@ -47,24 +75,6 @@ class Repository
)
end
- def exists?
- return @exists unless @exists.nil?
-
- @exists = cache.fetch(:exists?) do
- begin
- raw_repository && raw_repository.rugged ? true : false
- rescue Gitlab::Git::Repository::NoRepository
- false
- end
- end
- end
-
- def empty?
- return @empty unless @empty.nil?
-
- @empty = cache.fetch(:empty?) { raw_repository.empty? }
- end
-
#
# Git repository can contains some hidden refs like:
# /refs/notes/*
@@ -75,24 +85,22 @@ class Repository
# This method return true if repository contains some content visible in project page.
#
def has_visible_content?
- return @has_visible_content unless @has_visible_content.nil?
-
- @has_visible_content = cache.fetch(:has_visible_content?) do
- branch_count > 0
- end
+ branch_count > 0
end
def commit(ref = 'HEAD')
return nil unless exists?
+
commit =
if ref.is_a?(Gitlab::Git::Commit)
ref
else
Gitlab::Git::Commit.find(raw_repository, ref)
end
+
commit = ::Commit.new(commit, @project) if commit
commit
- rescue Rugged::OdbError
+ rescue Rugged::OdbError, Rugged::TreeError
nil
end
@@ -184,8 +192,9 @@ class Repository
options = { message: message, tagger: user_to_committer(user) } if message
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- rugged.tags.create(tag_name, target, options)
+ GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service|
+ raw_tag = rugged.tags.create(tag_name, target, options)
+ service.newrev = raw_tag.target_id
end
find_tag(tag_name)
@@ -222,16 +231,14 @@ class Repository
branch_names + tag_names
end
- def branch_names
- @branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) }
- end
-
def branch_exists?(branch_name)
branch_names.include?(branch_name)
end
def ref_exists?(ref)
rugged.references.exist?(ref)
+ rescue Rugged::ReferenceError
+ false
end
def update_ref!(name, newrev, oldrev)
@@ -239,7 +246,7 @@ class Repository
# offer 'compare and swap' ref updates. Without compare-and-swap we can
# (and have!) accidentally reset the ref to an earlier state, clobbering
# commits. See also https://github.com/libgit2/libgit2/issues/1534.
- command = %w[git update-ref --stdin -z]
+ command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
_, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
end
@@ -270,39 +277,7 @@ class Repository
end
def kept_around?(sha)
- begin
- ref_exists?(keep_around_ref_name(sha))
- rescue Rugged::ReferenceError
- false
- end
- end
-
- def tag_names
- cache.fetch(:tag_names) { raw_repository.tag_names }
- end
-
- def commit_count
- cache.fetch(:commit_count) do
- begin
- raw_repository.commit_count(self.root_ref)
- rescue
- 0
- end
- end
- end
-
- def branch_count
- @branch_count ||= cache.fetch(:branch_count) { branches.size }
- end
-
- def tag_count
- @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
- end
-
- # Return repo size in megabytes
- # Cached in redis
- def size
- cache.fetch(:size) { raw_repository.size }
+ ref_exists?(keep_around_ref_name(sha))
end
def diverging_commit_counts(branch)
@@ -320,48 +295,55 @@ class Repository
end
end
- # Keys for data that can be affected for any commit push.
- def cache_keys
- %i(size commit_count
- readme version contribution_guide changelog
- license_blob license_key gitignore koding_yml)
+ def expire_tags_cache
+ expire_method_caches(%i(tag_names tag_count))
+ @tags = nil
end
- # Keys for data on branch/tag operations.
- def cache_keys_for_branches_and_tags
- %i(branch_names tag_names branch_count tag_count)
+ def expire_branches_cache
+ expire_method_caches(%i(branch_names branch_count))
+ @local_branches = nil
end
- def build_cache
- (cache_keys + cache_keys_for_branches_and_tags).each do |key|
- unless cache.exist?(key)
- send(key)
- end
- end
+ def expire_statistics_caches
+ expire_method_caches(%i(size commit_count))
end
- def expire_tags_cache
- cache.expire(:tag_names)
- @tags = nil
+ def expire_all_method_caches
+ expire_method_caches(CACHED_METHODS)
end
- def expire_branches_cache
- cache.expire(:branch_names)
- @branch_names = nil
- @local_branches = nil
+ # Expires the caches of a specific set of methods
+ def expire_method_caches(methods)
+ methods.each do |key|
+ cache.expire(key)
+
+ ivar = cache_instance_variable_name(key)
+
+ remove_instance_variable(ivar) if instance_variable_defined?(ivar)
+ end
end
- def expire_cache(branch_name = nil, revision = nil)
- cache_keys.each do |key|
- cache.expire(key)
+ def expire_avatar_cache
+ expire_method_caches(%i(avatar))
+ end
+
+ # Refreshes the method caches of this repository.
+ #
+ # types - An Array of file types (e.g. `:readme`) used to refresh extra
+ # caches.
+ def refresh_method_caches(types)
+ to_refresh = []
+
+ types.each do |type|
+ methods = METHOD_CACHES_FOR_FILE_TYPES[type.to_sym]
+
+ to_refresh.concat(Array(methods)) if methods
end
- expire_branch_cache(branch_name)
- expire_avatar_cache(branch_name, revision)
+ expire_method_caches(to_refresh)
- # This ensures this particular cache is flushed after the first commit to a
- # new repository.
- expire_emptiness_caches if empty?
+ to_refresh.each { |method| send(method) }
end
def expire_branch_cache(branch_name = nil)
@@ -380,68 +362,32 @@ class Repository
end
def expire_root_ref_cache
- cache.expire(:root_ref)
- @root_ref = nil
+ expire_method_caches(%i(root_ref))
end
# Expires the cache(s) used to determine if a repository is empty or not.
def expire_emptiness_caches
- cache.expire(:empty?)
- @empty = nil
-
- expire_has_visible_content_cache
- end
+ return unless empty?
- def expire_has_visible_content_cache
- cache.expire(:has_visible_content?)
- @has_visible_content = nil
- end
-
- def expire_branch_count_cache
- cache.expire(:branch_count)
- @branch_count = nil
- end
-
- def expire_tag_count_cache
- cache.expire(:tag_count)
- @tag_count = nil
+ expire_method_caches(%i(empty?))
end
def lookup_cache
@lookup_cache ||= {}
end
- def expire_avatar_cache(branch_name = nil, revision = nil)
- # Avatars are pulled from the default branch, thus if somebody pushes to a
- # different branch there's no need to expire anything.
- return if branch_name && branch_name != root_ref
-
- # We don't want to flush the cache if the commit didn't actually make any
- # changes to any of the possible avatar files.
- if revision && commit = self.commit(revision)
- return unless commit.raw_diffs(deltas_only: true).
- any? { |diff| AVATAR_FILES.include?(diff.new_path) }
- end
-
- cache.expire(:avatar)
-
- @avatar = nil
- end
-
def expire_exists_cache
- cache.expire(:exists?)
- @exists = nil
+ expire_method_caches(%i(exists?))
end
# expire cache that doesn't depend on repository data (when expiring)
def expire_content_cache
expire_tags_cache
- expire_tag_count_cache
expire_branches_cache
- expire_branch_count_cache
expire_root_ref_cache
expire_emptiness_caches
expire_exists_cache
+ expire_statistics_caches
end
# Runs code after a repository has been created.
@@ -456,9 +402,8 @@ class Repository
# Runs code just before a repository is deleted.
def before_delete
expire_exists_cache
-
- expire_cache if exists?
-
+ expire_all_method_caches
+ expire_branch_cache if exists?
expire_content_cache
repository_event(:remove_repository)
@@ -475,9 +420,9 @@ class Repository
# Runs code before pushing (= creating or removing) a tag.
def before_push_tag
- expire_cache
+ expire_statistics_caches
+ expire_emptiness_caches
expire_tags_cache
- expire_tag_count_cache
repository_event(:push_tag)
end
@@ -485,7 +430,7 @@ class Repository
# Runs code before removing a tag.
def before_remove_tag
expire_tags_cache
- expire_tag_count_cache
+ expire_statistics_caches
repository_event(:remove_tag)
end
@@ -494,15 +439,22 @@ class Repository
expire_content_cache
end
+ # Runs code after the HEAD of a repository is changed.
+ def after_change_head
+ expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
+ end
+
# Runs code after a repository has been forked/imported.
def after_import
expire_content_cache
- build_cache
+ expire_tags_cache
+ expire_branches_cache
end
# Runs code after a new commit has been pushed.
- def after_push_commit(branch_name, revision)
- expire_cache(branch_name, revision)
+ def after_push_commit(branch_name)
+ expire_statistics_caches
+ expire_branch_cache(branch_name)
repository_event(:push_commit, branch: branch_name)
end
@@ -510,8 +462,6 @@ class Repository
# Runs code after a new branch has been created.
def after_create_branch
expire_branches_cache
- expire_has_visible_content_cache
- expire_branch_count_cache
repository_event(:push_branch)
end
@@ -525,8 +475,6 @@ class Repository
# Runs code after an existing branch has been removed.
def after_remove_branch
- expire_has_visible_content_cache
- expire_branch_count_cache
expire_branches_cache
end
@@ -553,86 +501,127 @@ class Repository
Gitlab::Git::Blob.raw(self, oid)
end
+ def root_ref
+ if raw_repository
+ raw_repository.root_ref
+ else
+ # When the repo does not exist we raise this error so no data is cached.
+ raise Rugged::ReferenceError
+ end
+ end
+ cache_method :root_ref
+
+ def exists?
+ refs_directory_exists?
+ end
+ cache_method :exists?
+
+ def empty?
+ raw_repository.empty?
+ end
+ cache_method :empty?
+
+ # The size of this repository in megabytes.
+ def size
+ exists? ? raw_repository.size : 0.0
+ end
+ cache_method :size, fallback: 0.0
+
+ def commit_count
+ root_ref ? raw_repository.commit_count(root_ref) : 0
+ end
+ cache_method :commit_count, fallback: 0
+
+ def branch_names
+ branches.map(&:name)
+ end
+ cache_method :branch_names, fallback: []
+
+ def tag_names
+ raw_repository.tag_names
+ end
+ cache_method :tag_names, fallback: []
+
+ def branch_count
+ branches.size
+ end
+ cache_method :branch_count, fallback: 0
+
+ def tag_count
+ raw_repository.rugged.tags.count
+ end
+ cache_method :tag_count, fallback: 0
+
+ def avatar
+ if tree = file_on_head(:avatar)
+ tree.path
+ end
+ end
+ cache_method :avatar
+
def readme
- cache.fetch(:readme) { tree(:head).readme }
+ if head = tree(:head)
+ head.readme
+ end
end
+ cache_method :readme
def version
- cache.fetch(:version) do
- tree(:head).blobs.find do |file|
- file.name.casecmp('version').zero?
- end
- end
+ file_on_head(:version)
end
+ cache_method :version
def contribution_guide
- cache.fetch(:contribution_guide) do
- tree(:head).blobs.find do |file|
- file.contributing?
- end
- end
+ file_on_head(:contributing)
end
+ cache_method :contribution_guide
def changelog
- cache.fetch(:changelog) do
- file_on_head(/\A(changelog|history|changes|news)/i)
- end
+ file_on_head(:changelog)
end
+ cache_method :changelog
def license_blob
- return nil unless head_exists?
-
- cache.fetch(:license_blob) do
- file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i)
- end
+ file_on_head(:license)
end
+ cache_method :license_blob
def license_key
- return nil unless head_exists?
+ return unless exists?
- cache.fetch(:license_key) do
- Licensee.license(path).try(:key)
- end
+ Licensee.license(path).try(:key)
end
+ cache_method :license_key
def gitignore
- return nil if !exists? || empty?
-
- cache.fetch(:gitignore) do
- file_on_head(/\A\.gitignore\z/)
- end
+ file_on_head(:gitignore)
end
+ cache_method :gitignore
def koding_yml
- return nil unless head_exists?
-
- cache.fetch(:koding_yml) do
- file_on_head(/\A\.koding\.yml\z/)
- end
+ file_on_head(:koding)
end
+ cache_method :koding_yml
def gitlab_ci_yml
- return nil unless head_exists?
-
- @gitlab_ci_yml ||= tree(:head).blobs.find do |file|
- file.name == '.gitlab-ci.yml'
- end
- rescue Rugged::ReferenceError
- # For unknow reason spinach scenario "Scenario: I change project path"
- # lead to "Reference 'HEAD' not found" exception from Repository#empty?
- nil
+ file_on_head(:gitlab_ci)
end
+ cache_method :gitlab_ci_yml
def head_commit
@head_commit ||= commit(self.root_ref)
end
def head_tree
- @head_tree ||= Tree.new(self, head_commit.sha, nil)
+ if head_commit
+ @head_tree ||= Tree.new(self, head_commit.sha, nil)
+ end
end
- def tree(sha = :head, path = nil)
+ def tree(sha = :head, path = nil, recursive: false)
if sha == :head
+ return unless head_commit
+
if path.nil?
return head_tree
else
@@ -640,7 +629,7 @@ class Repository
end
end
- Tree.new(self, sha, path)
+ Tree.new(self, sha, path, recursive: recursive)
end
def blob_at_branch(branch_name, path)
@@ -670,11 +659,19 @@ class Repository
end
def last_commit_for_path(sha, path)
- args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
- sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
+ sha = last_commit_id_for_path(sha, path)
commit(sha)
end
+ def last_commit_id_for_path(sha, path)
+ key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}"
+
+ cache.fetch(key) do
+ args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
+ Gitlab::Popen.popen(args, path_to_repo).first.strip
+ end
+ end
+
def next_branch(name, opts = {})
branch_ids = self.branch_names.map do |n|
next 1 if n == name
@@ -782,10 +779,6 @@ class Repository
@tags ||= raw_repository.tags
end
- def root_ref
- @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
- end
-
def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
update_branch_with_hooks(user, branch) do |ref|
options = {
@@ -970,7 +963,7 @@ class Repository
update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
- message: commit.revert_message,
+ message: commit.revert_message(user),
author: committer,
committer: committer,
tree: revert_tree_id,
@@ -1063,16 +1056,25 @@ class Repository
merge_base(ancestor_id, descendant_id) == ancestor_id
end
- def search_files(query, ref)
- unless exists? && has_visible_content? && query.present?
- return []
- end
+ def empty_repo?
+ !exists? || !has_visible_content?
+ end
+
+ def search_files_by_content(query, ref)
+ return [] if empty_repo? || query.blank?
offset = 2
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
+ def search_files_by_name(query, ref)
+ return [] if empty_repo? || query.blank?
+
+ args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
+ Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
+ end
+
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
@@ -1134,28 +1136,55 @@ class Repository
end
end
- def avatar
- return nil unless exists?
+ # Caches the supplied block both in a cache and in an instance variable.
+ #
+ # The cache key and instance variable are named the same way as the value of
+ # the `key` argument.
+ #
+ # This method will return `nil` if the corresponding instance variable is also
+ # set to `nil`. This ensures we don't keep yielding the block when it returns
+ # `nil`.
+ #
+ # key - The name of the key to cache the data in.
+ # fallback - A value to fall back to in the event of a Git error.
+ def cache_method_output(key, fallback: nil, &block)
+ ivar = cache_instance_variable_name(key)
- @avatar ||= cache.fetch(:avatar) do
- AVATAR_FILES.find do |file|
- blob_at_branch(root_ref, file)
+ if instance_variable_defined?(ivar)
+ instance_variable_get(ivar)
+ else
+ begin
+ instance_variable_set(ivar, cache.fetch(key, &block))
+ rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
+ # if e.g. HEAD or the entire repository doesn't exist we want to
+ # gracefully handle this and not cache anything.
+ fallback
end
end
end
- private
+ def cache_instance_variable_name(key)
+ :"@#{key.to_s.tr('?!', '')}"
+ end
- def cache
- @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
+ def file_on_head(type)
+ if head = tree(:head)
+ head.blobs.find do |file|
+ Gitlab::FileDetector.type_of(file.name) == type
+ end
+ end
end
- def head_exists?
- exists? && !empty? && !rugged.head_unborn?
+ private
+
+ def refs_directory_exists?
+ return false unless path_with_namespace
+
+ File.exist?(File.join(path_to_repo, 'refs'))
end
- def file_on_head(regex)
- tree(:head).blobs.find { |file| file.name =~ regex }
+ def cache
+ @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
end
def tags_sorted_by_committed_date
diff --git a/app/models/route.rb b/app/models/route.rb
new file mode 100644
index 00000000000..caf596efa79
--- /dev/null
+++ b/app/models/route.rb
@@ -0,0 +1,22 @@
+class Route < ActiveRecord::Base
+ belongs_to :source, polymorphic: true
+
+ validates :source, presence: true
+
+ validates :path,
+ length: { within: 1..255 },
+ presence: true,
+ uniqueness: { case_sensitive: false }
+
+ after_update :rename_children, if: :path_changed?
+
+ def rename_children
+ # We update each row separately because MySQL does not have regexp_replace.
+ # rubocop:disable Rails/FindEach
+ Route.where('path LIKE ?', "#{path_was}/%").each do |route|
+ # Note that update column skips validation and callbacks.
+ # We need this to avoid recursive call of rename_children method
+ route.update_column(:path, route.path.sub(path_was, path))
+ end
+ end
+end
diff --git a/app/models/service.rb b/app/models/service.rb
index 625fbc48302..043be222f3a 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -8,6 +8,7 @@ class Service < ActiveRecord::Base
default_value_for :push_events, true
default_value_for :issues_events, true
default_value_for :confidential_issues_events, true
+ default_value_for :commit_events, true
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
@@ -75,6 +76,11 @@ class Service < ActiveRecord::Base
def to_param
# implement inside child
+ self.class.to_param
+ end
+
+ def self.to_param
+ raise NotImplementedError
end
def fields
@@ -91,7 +97,11 @@ class Service < ActiveRecord::Base
end
def event_names
- supported_events.map { |event| "#{event}_events" }
+ self.class.event_names
+ end
+
+ def self.event_names
+ self.supported_events.map { |event| "#{event}_events" }
end
def event_field(event)
@@ -103,6 +113,10 @@ class Service < ActiveRecord::Base
end
def supported_events
+ self.class.supported_events
+ end
+
+ def self.supported_events
%w(push tag_push issue confidential_issue merge_request wiki_page)
end
@@ -202,7 +216,6 @@ class Service < ActiveRecord::Base
bamboo
buildkite
builds_email
- pipelines_email
bugzilla
campfire
custom_issue_tracker
@@ -214,19 +227,24 @@ class Service < ActiveRecord::Base
hipchat
irker
jira
+ kubernetes
+ mattermost_slash_commands
+ mattermost
+ pipelines_email
pivotaltracker
pushover
redmine
+ slack_slash_commands
slack
teamcity
]
end
- def self.create_from_template(project_id, template)
+ def self.build_from_template(project_id, template)
service = template.dup
service.template = false
service.project_id = project_id
- service if service.save
+ service
end
private
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 2373b445009..771a7350556 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -6,6 +6,7 @@ class Snippet < ActiveRecord::Base
include Referable
include Sortable
include Awardable
+ include Mentionable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content
@@ -26,9 +27,9 @@ class Snippet < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true, allow_nil: true
validates :author, presence: true
- validates :title, presence: true, length: { within: 0..255 }
+ validates :title, presence: true, length: { maximum: 255 }
validates :file_name,
- length: { within: 0..255 },
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.file_name_regex,
message: Gitlab::Regex.file_name_regex_message }
@@ -63,14 +64,14 @@ class Snippet < ActiveRecord::Base
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
- def to_reference(from_project = nil)
+ def to_reference(from_project = nil, full: false)
reference = "#{self.class.reference_prefix}#{id}"
- if cross_project_reference?(from_project)
- reference = project.to_reference + reference
+ if project.present?
+ "#{project.to_reference(from_project, full: full)}#{reference}"
+ else
+ reference
end
-
- reference
end
def self.content_types
@@ -93,6 +94,10 @@ class Snippet < ActiveRecord::Base
0
end
+ def file_name
+ super.to_s
+ end
+
# alias for compatibility with blobs and highlighting
def path
file_name
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index 3b8aa1eb866..17869c8bac2 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -1,8 +1,9 @@
class Subscription < ActiveRecord::Base
belongs_to :user
+ belongs_to :project
belongs_to :subscribable, polymorphic: true
- validates :user_id,
- uniqueness: { scope: [:subscribable_id, :subscribable_type] },
- presence: true
+ validates :user, :subscribable, presence: true
+
+ validates :project_id, uniqueness: { scope: [:subscribable_id, :subscribable_type, :user_id] }
end
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
new file mode 100644
index 00000000000..f768c4e3da5
--- /dev/null
+++ b/app/models/timelog.rb
@@ -0,0 +1,6 @@
+class Timelog < ActiveRecord::Base
+ validates :time_spent, :user, presence: true
+
+ belongs_to :trackable, polymorphic: true
+ belongs_to :user
+end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index f5ade1cc293..4c99aa0d3be 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -6,13 +6,15 @@ class Todo < ActiveRecord::Base
BUILD_FAILED = 3
MARKED = 4
APPROVAL_REQUIRED = 5 # This is an EE-only feature
+ UNMERGEABLE = 6
ACTION_NAMES = {
ASSIGNED => :assigned,
MENTIONED => :mentioned,
BUILD_FAILED => :build_failed,
MARKED => :marked,
- APPROVAL_REQUIRED => :approval_required
+ APPROVAL_REQUIRED => :approval_required,
+ UNMERGEABLE => :unmergeable
}
belongs_to :author, class_name: "User"
@@ -66,6 +68,10 @@ class Todo < ActiveRecord::Base
end
end
+ def unmergeable?
+ action == UNMERGEABLE
+ end
+
def build_failed?
action == BUILD_FAILED
end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 7c4ed6e393b..fe148b0ec65 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -3,21 +3,24 @@ class Tree
attr_accessor :repository, :sha, :path, :entries
- def initialize(repository, sha, path = '/')
+ def initialize(repository, sha, path = '/', recursive: false)
path = '/' if path.blank?
@repository = repository
@sha = sha
@path = path
+ @recursive = recursive
git_repo = @repository.raw_repository
- @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path)
+ @entries = get_entries(git_repo, @sha, @path, recursive: @recursive)
end
def readme
return @readme if defined?(@readme)
- available_readmes = blobs.select(&:readme?)
+ available_readmes = blobs.select do |blob|
+ Gitlab::FileDetector.type_of(blob.name) == :readme
+ end
previewable_readmes = available_readmes.select do |blob|
previewable?(blob.name)
@@ -58,4 +61,21 @@ class Tree
def sorted_entries
trees + blobs + submodules
end
+
+ private
+
+ def get_entries(git_repo, sha, path, recursive: false)
+ current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path)
+ ordered_entries = []
+
+ current_path_entries.each do |entry|
+ ordered_entries << entry
+
+ if recursive && entry.dir?
+ ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true))
+ end
+ end
+
+ ordered_entries
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3813df6684e..06dd98a3188 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -56,6 +56,7 @@ class User < ActiveRecord::Base
has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
+ has_many :chat_names, dependent: :destroy
# Groups
has_many :members, dependent: :destroy
@@ -72,6 +73,8 @@ class User < ActiveRecord::Base
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
+ has_many :project_authorizations
+ has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id
has_many :issues, dependent: :destroy, foreign_key: :author_id
@@ -96,6 +99,7 @@ class User < ActiveRecord::Base
#
# Note: devise :validatable above adds validations for :email and :password
validates :name, presence: true
+ validates_confirmation_of :email
validates :notification_email, presence: true
validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
@@ -173,8 +177,10 @@ class User < ActiveRecord::Base
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
- scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
+ scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
+ scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
+ scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
@@ -202,8 +208,8 @@ class User < ActiveRecord::Base
def sort(method)
case method.to_s
- when 'recent_sign_in' then reorder(last_sign_in_at: :desc)
- when 'oldest_sign_in' then reorder(last_sign_in_at: :asc)
+ when 'recent_sign_in' then order_recent_sign_in
+ when 'oldest_sign_in' then order_oldest_sign_in
else
order_by(method)
end
@@ -226,19 +232,19 @@ class User < ActiveRecord::Base
def filter(filter_name)
case filter_name
when 'admins'
- self.admins
+ admins
when 'blocked'
- self.blocked
+ blocked
when 'two_factor_disabled'
- self.without_two_factor
+ without_two_factor
when 'two_factor_enabled'
- self.with_two_factor
+ with_two_factor
when 'wop'
- self.without_projects
+ without_projects
when 'external'
- self.external
+ external
else
- self.active
+ active
end
end
@@ -288,8 +294,12 @@ class User < ActiveRecord::Base
end
end
+ def find_by_username(username)
+ iwhere(username: username).take
+ end
+
def find_by_username!(username)
- find_by!('lower(username) = ?', username.downcase)
+ iwhere(username: username).take!
end
def find_by_personal_access_token(token_string)
@@ -297,19 +307,11 @@ class User < ActiveRecord::Base
personal_access_token.user if personal_access_token
end
- def by_username_or_id(name_or_id)
- find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
- end
-
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
end
- def build_user(attrs = {})
- User.new(attrs)
- end
-
def reference_prefix
'@'
end
@@ -331,12 +333,12 @@ class User < ActiveRecord::Base
username
end
- def to_reference(_from_project = nil, _target_project = nil)
+ def to_reference(_from_project = nil, target_project: nil, full: nil)
"#{self.class.reference_prefix}#{username}"
end
def generate_password
- if self.force_random_password
+ if force_random_password
self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
end
end
@@ -377,56 +379,55 @@ class User < ActiveRecord::Base
end
def two_factor_otp_enabled?
- self.otp_required_for_login?
+ otp_required_for_login?
end
def two_factor_u2f_enabled?
- self.u2f_registrations.exists?
+ u2f_registrations.exists?
end
def namespace_uniq
# Return early if username already failed the first uniqueness validation
- return if self.errors.key?(:username) &&
- self.errors[:username].include?('has already been taken')
+ return if errors.key?(:username) &&
+ errors[:username].include?('has already been taken')
- namespace_name = self.username
- existing_namespace = Namespace.by_path(namespace_name)
- if existing_namespace && existing_namespace != self.namespace
- self.errors.add(:username, 'has already been taken')
+ existing_namespace = Namespace.by_path(username)
+ if existing_namespace && existing_namespace != namespace
+ errors.add(:username, 'has already been taken')
end
end
def avatar_type
- unless self.avatar.image?
- self.errors.add :avatar, "only images allowed"
+ unless avatar.image?
+ errors.add :avatar, "only images allowed"
end
end
def unique_email
- if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email)
- self.errors.add(:email, 'has already been taken')
+ if !emails.exists?(email: email) && Email.exists?(email: email)
+ errors.add(:email, 'has already been taken')
end
end
def owns_notification_email
- return if self.temp_oauth_email?
+ return if temp_oauth_email?
- self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
+ errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
end
def owns_public_email
- return if self.public_email.blank?
+ return if public_email.blank?
- self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email)
+ errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end
def update_emails_with_primary_email
- primary_email_record = self.emails.find_by(email: self.email)
+ primary_email_record = emails.find_by(email: email)
if primary_email_record
primary_email_record.destroy
- self.emails.create(email: self.email_was)
+ emails.create(email: email_was)
- self.update_secondary_emails!
+ update_secondary_emails!
end
end
@@ -438,11 +439,32 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
- # Returns projects user is authorized to access.
- #
- # If you change the logic of this method, please also update `Project#authorized_for_user`
+ def refresh_authorized_projects
+ Users::RefreshAuthorizedProjectsService.new(self).execute
+ end
+
+ def remove_project_authorizations(project_ids)
+ project_authorizations.where(project_id: project_ids).delete_all
+ end
+
+ def set_authorized_projects_column
+ unless authorized_projects_populated
+ update_column(:authorized_projects_populated, true)
+ end
+ end
+
def authorized_projects(min_access_level = nil)
- Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
+ refresh_authorized_projects unless authorized_projects_populated
+
+ # We're overriding an association, so explicitly call super with no arguments or it would be passed as `force_reload` to the association
+ projects = super()
+ projects = projects.where('project_authorizations.access_level >= ?', min_access_level) if min_access_level
+
+ projects
+ end
+
+ def authorized_project?(project, min_access_level = nil)
+ authorized_projects(min_access_level).exists?({ id: project.id })
end
# Returns the projects this user has reporter (or greater) access to, limited
@@ -456,8 +478,9 @@ class User < ActiveRecord::Base
end
def viewable_starred_projects
- starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})",
- [Project::PUBLIC, Project::INTERNAL])
+ starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (?)",
+ [Project::PUBLIC, Project::INTERNAL],
+ authorized_projects.select(:project_id))
end
def owned_projects
@@ -478,7 +501,7 @@ class User < ActiveRecord::Base
end
def require_ssh_key?
- keys.count == 0
+ keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
end
def require_password?
@@ -580,7 +603,7 @@ class User < ActiveRecord::Base
end
def project_deploy_keys
- DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id)
+ DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
end
def accessible_deploy_keys
@@ -596,38 +619,38 @@ class User < ActiveRecord::Base
end
def sanitize_attrs
- %w(name username skype linkedin twitter).each do |attr|
- value = self.send(attr)
- self.send("#{attr}=", Sanitize.clean(value)) if value.present?
+ %w[name username skype linkedin twitter].each do |attr|
+ value = public_send(attr)
+ public_send("#{attr}=", Sanitize.clean(value)) if value.present?
end
end
def set_notification_email
- if self.notification_email.blank? || !self.all_emails.include?(self.notification_email)
- self.notification_email = self.email
+ if notification_email.blank? || !all_emails.include?(notification_email)
+ self.notification_email = email
end
end
def set_public_email
- if self.public_email.blank? || !self.all_emails.include?(self.public_email)
+ if public_email.blank? || !all_emails.include?(public_email)
self.public_email = ''
end
end
def update_secondary_emails!
- self.set_notification_email
- self.set_public_email
- self.save if self.notification_email_changed? || self.public_email_changed?
+ set_notification_email
+ set_public_email
+ save if notification_email_changed? || public_email_changed?
end
def set_projects_limit
# `User.select(:id)` raises
# `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
# without this safeguard!
- return unless self.has_attribute?(:projects_limit)
+ return unless has_attribute?(:projects_limit)
connection_default_value_defined = new_record? && !projects_limit_changed?
- return unless self.projects_limit.nil? || connection_default_value_defined
+ return unless projects_limit.nil? || connection_default_value_defined
self.projects_limit = current_application_settings.default_projects_limit
end
@@ -657,7 +680,7 @@ class User < ActiveRecord::Base
def with_defaults
User.defaults.each do |k, v|
- self.send("#{k}=", v)
+ public_send("#{k}=", v)
end
self
@@ -668,20 +691,6 @@ class User < ActiveRecord::Base
project.project_member(self)
end
- # Reset project events cache related to this user
- #
- # Since we do cache @event we need to reset cache in special cases:
- # * when the user changes their avatar
- # Events cache stored like events/23-20130109142513.
- # The cache key includes updated_at timestamp.
- # Thus it will automatically generate a new fragment
- # when the event is updated because the key changes.
- def reset_events_cache
- Event.where(author_id: self.id).
- order('id DESC').limit(1000).
- update_all(updated_at: Time.now)
- end
-
def full_website_url
return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
@@ -710,8 +719,8 @@ class User < ActiveRecord::Base
def all_emails
all_emails = []
- all_emails << self.email unless self.temp_oauth_email?
- all_emails.concat(self.emails.map(&:email))
+ all_emails << email unless temp_oauth_email?
+ all_emails.concat(emails.map(&:email))
all_emails
end
@@ -725,21 +734,21 @@ class User < ActiveRecord::Base
def ensure_namespace_correct
# Ensure user has namespace
- self.create_namespace!(path: self.username, name: self.username) unless self.namespace
+ create_namespace!(path: username, name: username) unless namespace
- if self.username_changed?
- self.namespace.update_attributes(path: self.username, name: self.username)
+ if username_changed?
+ namespace.update_attributes(path: username, name: username)
end
end
def post_create_hook
- log_info("User \"#{self.name}\" (#{self.email}) was created")
- notification_service.new_user(self, @reset_token) if self.created_by_id
+ log_info("User \"#{name}\" (#{email}) was created")
+ notification_service.new_user(self, @reset_token) if created_by_id
system_hook_service.execute_hooks_for(self, :create)
end
def post_destroy_hook
- log_info("User \"#{self.name}\" (#{self.email}) was removed")
+ log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
end
@@ -783,7 +792,7 @@ class User < ActiveRecord::Base
end
def oauth_authorized_tokens
- Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
+ Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
end
# Returns the projects a user contributed to in the last year.
@@ -887,20 +896,6 @@ class User < ActiveRecord::Base
private
- def projects_union(min_access_level = nil)
- relations = [personal_projects.select(:id),
- groups_projects.select(:id),
- projects.select(:id),
- groups.joins(:shared_projects).select(:project_id)]
-
- if min_access_level
- scope = { access_level: Gitlab::Access.all_values.select { |access| access >= min_access_level } }
- relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) }
- end
-
- Gitlab::SQL::Union.new(relations)
- end
-
def ci_projects_union
scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
groups = groups_projects.where(members: scope)
@@ -916,7 +911,7 @@ class User < ActiveRecord::Base
end
def ensure_external_user_rights
- return unless self.external?
+ return unless external?
self.can_create_group = false
self.projects_limit = 0
@@ -928,7 +923,7 @@ class User < ActiveRecord::Base
if current_application_settings.domain_blacklist_enabled?
blocked_domains = current_application_settings.domain_blacklist
- if domain_matches?(blocked_domains, self.email)
+ if domain_matches?(blocked_domains, email)
error = 'is not from an allowed domain.'
valid = false
end
@@ -936,7 +931,7 @@ class User < ActiveRecord::Base
allowed_domains = current_application_settings.domain_whitelist
unless allowed_domains.blank?
- if domain_matches?(allowed_domains, self.email)
+ if domain_matches?(allowed_domains, email)
valid = true
else
error = "domain is not authorized for sign-up"
@@ -944,7 +939,7 @@ class User < ActiveRecord::Base
end
end
- self.errors.add(:email, error) unless valid
+ errors.add(:email, error) unless valid
valid
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 118c100ca11..b9f1c29c32e 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -53,6 +53,10 @@ class BasePolicy
def self.class_for(subject)
return GlobalPolicy if subject.nil?
+ if subject.class.try(:presenter?)
+ subject = subject.subject
+ end
+
subject.class.ancestors.each do |klass|
next unless klass.name
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 8b25332b73c..7b1752df0e1 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -1,6 +1,8 @@
module Ci
class BuildPolicy < CommitStatusPolicy
def rules
+ can! :read_build if @subject.project.public_builds?
+
super
# If we can't read build we should also not have that
diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb
index 62335527654..5a3fe814b77 100644
--- a/app/policies/group_member_policy.rb
+++ b/app/policies/group_member_policy.rb
@@ -15,5 +15,11 @@ class GroupMemberPolicy < BasePolicy
elsif @user == target_user
can! :destroy_group_member
end
+
+ additional_rules!
+ end
+
+ def additional_rules!
+ # This is meant to be overriden in EE
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index b65fb68cd88..0be6e113655 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -4,7 +4,7 @@ class GroupPolicy < BasePolicy
return unless @user
globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
- member = @subject.users.include?(@user)
+ member = @subject.users_with_parents.include?(@user)
owner = @user.admin? || @subject.has_owner?(@user)
master = owner || @subject.has_master?(@user)
@@ -33,6 +33,8 @@ class GroupPolicy < BasePolicy
if globally_viewable && @subject.request_access_enabled && !member
can! :request_access
end
+
+ additional_rules!(master)
end
def can_read_group?
@@ -43,4 +45,8 @@ class GroupPolicy < BasePolicy
GroupProjectsFinder.new(@subject).execute(@user).any?
end
+
+ def additional_rules!(master)
+ # This is meant to be overriden in EE
+ end
end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index 83847466ee2..5326061bd07 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -12,7 +12,7 @@ class NotePolicy < BasePolicy
end
if @subject.for_merge_request? &&
- @subject.noteable.author == @user
+ @subject.noteable.author == @user
can! :resolve_note
end
end
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 46c5aa1a5be..d3913986cd8 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.author == @user
can! :read_personal_snippet
can! :update_personal_snippet
+ can! :destroy_personal_snippet
can! :admin_personal_snippet
end
+ unless @user.external?
+ can! :create_personal_snippet
+ end
+
if @subject.internal? && !@user.external?
can! :read_personal_snippet
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 1ee31023e26..71ef8901932 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -3,7 +3,7 @@ class ProjectPolicy < BasePolicy
team_access!(user)
owner = project.owner == user ||
- (project.group && project.group.has_owner?(user))
+ (project.group && project.group.has_owner?(user))
owner_access! if user.admin? || owner
team_member_owner_access! if owner
@@ -12,11 +12,8 @@ class ProjectPolicy < BasePolicy
guest_access!
public_access!
- # Allow to read builds for internal projects
- can! :read_build if project.public_builds?
-
if project.request_access_enabled &&
- !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
+ !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
can! :request_access
end
end
@@ -46,10 +43,16 @@ class ProjectPolicy < BasePolicy
can! :create_note
can! :upload_file
can! :read_cycle_analytics
+
+ if project.public_builds?
+ can! :read_pipeline
+ can! :read_build
+ end
end
def reporter_access!
can! :download_code
+ can! :download_wiki_code
can! :fork_project
can! :create_project_snippet
can! :update_issue
@@ -168,9 +171,7 @@ class ProjectPolicy < BasePolicy
def disabled_features!
repository_enabled = project.feature_available?(:repository, user)
- unless project.feature_available?(:issues, user)
- cannot!(*named_abilities(:issue))
- end
+ block_issues_abilities
unless project.feature_available?(:merge_requests, user) && repository_enabled
cannot!(*named_abilities(:merge_request))
@@ -187,6 +188,7 @@ class ProjectPolicy < BasePolicy
unless project.feature_available?(:wiki, user) || project.has_external_wiki?
cannot!(*named_abilities(:wiki))
+ cannot!(:download_wiki_code)
end
unless project.feature_available?(:builds, user) && repository_enabled
@@ -226,6 +228,7 @@ class ProjectPolicy < BasePolicy
can! :read_commit_status
can! :read_container_image
can! :download_code
+ can! :download_wiki_code
can! :read_cycle_analytics
# NOTE: may be overridden by IssuePolicy
@@ -239,10 +242,19 @@ class ProjectPolicy < BasePolicy
def project_group_member?(user)
project.group &&
- (
- project.group.members.exists?(user_id: user.id) ||
- project.group.requesters.exists?(user_id: user.id)
- )
+ (
+ project.group.members_with_parents.exists?(user_id: user.id) ||
+ project.group.requesters.exists?(user_id: user.id)
+ )
+ end
+
+ def block_issues_abilities
+ unless project.feature_available?(:issues, user)
+ cannot! :read_issue if project.default_issues_tracker?
+ cannot! :create_issue
+ cannot! :update_issue
+ cannot! :admin_issue
+ end
end
def named_abilities(name)
diff --git a/app/presenters/README.md b/app/presenters/README.md
new file mode 100644
index 00000000000..3edd63451e7
--- /dev/null
+++ b/app/presenters/README.md
@@ -0,0 +1,154 @@
+# Presenters
+
+This type of class is responsible for giving the view an object which defines
+**view-related logic/data methods**. It is usually useful to extract such
+methods from models to presenters.
+
+## When to use a presenter?
+
+### When your view is full of logic
+
+When your view is full of logic (`if`, `else`, `select` on arrays etc.), it's
+time to create a presenter!
+
+### When your model has a lot of view-related logic/data methods
+
+When your model has a lot of view-related logic/data methods, you can easily
+move them to a presenter.
+
+## Why are we using presenters instead of helpers?
+
+We don't use presenters to generate complex view output that would rely on helpers.
+
+Presenters should be used for:
+
+- Data and logic methods that can be pulled & combined into single methods from
+ view. This can include loops extracted from views too. A good example is
+ https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7073/diffs.
+- Data and logic methods that can be pulled from models.
+- Simple text output methods: it's ok if the method returns a string, but not a
+ whole DOM element for which we'd need HAML, a view context, helpers etc.
+
+## Why use presenters instead of model concerns?
+
+We should strive to follow the single-responsibility principle, and view-related
+logic/data methods are definitely not the responsibility of models!
+
+Another reason is as follows:
+
+> Avoid using concerns and use presenters instead. Why? After all, concerns seem
+to be a core part of Rails and can DRY up code when shared among multiple models.
+Nonetheless, the main issue is that concerns don’t make the model object more
+cohesive. The code is just better organized. In other words, there’s no real
+change to the API of the model.
+
+– https://www.toptal.com/ruby-on-rails/decoupling-rails-components
+
+## Benefits
+
+By moving pure view-related logic/data methods from models & views to presenters,
+we gain the following benefits:
+
+- rules are more explicit and centralized in the presenter => improves security
+- testing is easier and faster as presenters are Plain Old Ruby Object (PORO)
+- views are more readable and maintainable
+- decreases number of CE -> EE merge conflicts since code is in separate files
+- moves the conflicts from views (not always obvious) to presenters (a lot easier to resolve)
+
+## What not to do with presenters?
+
+- Don't use helpers in presenters. Presenters are not aware of the view context.
+- Don't generate complex DOM elements, forms etc. with presenters. Presenters
+ can return simple data as texts, and URLs using URL helpers from
+ `Gitlab::Routing` but nothing much more fancy.
+
+## Implementation
+
+### Presenter definition
+
+Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which
+provides a `.presents` method which allows you to define an accessor for the
+presented object. It also includes common helpers like `Gitlab::Routing` and
+`Gitlab::Allowable`.
+
+```ruby
+class LabelPresenter < Gitlab::View::Presenter::Simple
+ presents :label
+
+ def text_color
+ label.color.to_s
+ end
+
+ def to_partial_path
+ 'projects/labels/show'
+ end
+end
+```
+
+In some cases, it can be more practical to transparently delegate all missing
+method calls to the presented object, in these cases, you can make your
+presenter inherit from `Gitlab::View::Presenter::Delegated`:
+
+```ruby
+class LabelPresenter < Gitlab::View::Presenter::Delegated
+ presents :label
+
+ def text_color
+ # color is delegated to label
+ color.to_s
+ end
+
+ def to_partial_path
+ 'projects/labels/show'
+ end
+end
+```
+
+### Presenter instantiation
+
+Instantiation must be done via the `Gitlab::View::Presenter::Factory` class which
+detects the presenter based on the presented subject's class.
+
+```ruby
+class Projects::LabelsController < Projects::ApplicationController
+ def edit
+ @label = Gitlab::View::Presenter::Factory
+ .new(@label, user: current_user)
+ .fabricate!
+ end
+end
+```
+
+You can also include the `Presentable` concern in the model:
+
+```ruby
+class Label
+ include Presentable
+end
+```
+
+and then in the controller:
+
+```ruby
+class Projects::LabelsController < Projects::ApplicationController
+ def edit
+ @label = @label.present(user: current_user)
+ end
+end
+```
+
+### Presenter usage
+
+```ruby
+%div{ class: @label.text_color }
+ = render partial: @label, label: @label
+```
+
+You can also present the model in the view:
+
+```ruby
+- label = @label.present(current_user)
+
+%div{ class: label.text_color }
+ = render partial: label, label: label
+```
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
new file mode 100644
index 00000000000..ed72ed14d72
--- /dev/null
+++ b/app/presenters/ci/build_presenter.rb
@@ -0,0 +1,15 @@
+module Ci
+ class BuildPresenter < Gitlab::View::Presenter::Delegated
+ presents :build
+
+ def erased_by_user?
+ # Build can be erased through API, therefore it does not have
+ # `erased_by` user assigned in that case.
+ erased? && erased_by
+ end
+
+ def erased_by_name
+ erased_by.name if erased_by_user?
+ end
+ end
+end
diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb
new file mode 100644
index 00000000000..a0db5b8f0f4
--- /dev/null
+++ b/app/serializers/analytics_build_entity.rb
@@ -0,0 +1,40 @@
+class AnalyticsBuildEntity < Grape::Entity
+ include RequestAwareEntity
+ include EntityDateHelper
+
+ expose :name
+ expose :id
+ expose :ref, as: :branch
+ expose :short_sha
+ expose :author, using: UserEntity
+
+ expose :started_at, as: :date do |build|
+ interval_in_words(build[:started_at])
+ end
+
+ expose :duration, as: :total_time do |build|
+ build.duration ? distance_of_time_as_hash(build.duration.to_f) : {}
+ end
+
+ expose :branch do
+ expose :ref, as: :name
+
+ expose :url do |build|
+ url_to(:namespace_project_tree, build, build.ref)
+ end
+ end
+
+ expose :url do |build|
+ url_to(:namespace_project_build, build)
+ end
+
+ expose :commit_url do |build|
+ url_to(:namespace_project_commit, build, build.sha)
+ end
+
+ private
+
+ def url_to(route, build, id = nil)
+ public_send("#{route}_url", build.project.namespace, build.project, id || build)
+ end
+end
diff --git a/app/serializers/analytics_build_serializer.rb b/app/serializers/analytics_build_serializer.rb
new file mode 100644
index 00000000000..f172d67d356
--- /dev/null
+++ b/app/serializers/analytics_build_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsBuildSerializer < BaseSerializer
+ entity AnalyticsBuildEntity
+end
diff --git a/app/serializers/analytics_commit_entity.rb b/app/serializers/analytics_commit_entity.rb
new file mode 100644
index 00000000000..402cecbfd08
--- /dev/null
+++ b/app/serializers/analytics_commit_entity.rb
@@ -0,0 +1,13 @@
+class AnalyticsCommitEntity < CommitEntity
+ include EntityDateHelper
+
+ expose :short_id, as: :short_sha
+
+ expose :total_time do |commit|
+ distance_of_time_as_hash(request.total_time.to_f)
+ end
+
+ unexpose :author_name
+ unexpose :author_email
+ unexpose :message
+end
diff --git a/app/serializers/analytics_commit_serializer.rb b/app/serializers/analytics_commit_serializer.rb
new file mode 100644
index 00000000000..cdbfecf2b70
--- /dev/null
+++ b/app/serializers/analytics_commit_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsCommitSerializer < BaseSerializer
+ entity AnalyticsCommitEntity
+end
diff --git a/app/serializers/analytics_generic_serializer.rb b/app/serializers/analytics_generic_serializer.rb
new file mode 100644
index 00000000000..9f4859e8410
--- /dev/null
+++ b/app/serializers/analytics_generic_serializer.rb
@@ -0,0 +1,7 @@
+class AnalyticsGenericSerializer < BaseSerializer
+ def represent(resource, opts = {})
+ resource.symbolize_keys!
+
+ super(resource, opts)
+ end
+end
diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb
new file mode 100644
index 00000000000..44c50f18613
--- /dev/null
+++ b/app/serializers/analytics_issue_entity.rb
@@ -0,0 +1,29 @@
+class AnalyticsIssueEntity < Grape::Entity
+ include RequestAwareEntity
+ include EntityDateHelper
+
+ expose :title
+ expose :author, using: UserEntity
+
+ expose :iid do |object|
+ object[:iid].to_s
+ end
+
+ expose :total_time do |object|
+ distance_of_time_as_hash(object[:total_time].to_f)
+ end
+
+ expose(:created_at) do |object|
+ interval_in_words(object[:created_at])
+ end
+
+ expose :url do |object|
+ url_to(:namespace_project_issue, id: object[:iid].to_s)
+ end
+
+ private
+
+ def url_to(route, id)
+ public_send("#{route}_url", request.project.namespace, request.project, id)
+ end
+end
diff --git a/app/serializers/analytics_issue_serializer.rb b/app/serializers/analytics_issue_serializer.rb
new file mode 100644
index 00000000000..4fb3e8f1bb4
--- /dev/null
+++ b/app/serializers/analytics_issue_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsIssueSerializer < AnalyticsGenericSerializer
+ entity AnalyticsIssueEntity
+end
diff --git a/app/serializers/analytics_merge_request_entity.rb b/app/serializers/analytics_merge_request_entity.rb
new file mode 100644
index 00000000000..888265eaa38
--- /dev/null
+++ b/app/serializers/analytics_merge_request_entity.rb
@@ -0,0 +1,7 @@
+class AnalyticsMergeRequestEntity < AnalyticsIssueEntity
+ expose :state
+
+ expose :url do |object|
+ url_to(:namespace_project_merge_request, id: object[:iid].to_s)
+ end
+end
diff --git a/app/serializers/analytics_merge_request_serializer.rb b/app/serializers/analytics_merge_request_serializer.rb
new file mode 100644
index 00000000000..4622a1dd855
--- /dev/null
+++ b/app/serializers/analytics_merge_request_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsMergeRequestSerializer < AnalyticsGenericSerializer
+ entity AnalyticsMergeRequestEntity
+end
diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb
new file mode 100644
index 00000000000..a559d0850c4
--- /dev/null
+++ b/app/serializers/analytics_stage_entity.rb
@@ -0,0 +1,10 @@
+class AnalyticsStageEntity < Grape::Entity
+ include EntityDateHelper
+
+ expose :title
+ expose :description
+
+ expose :median, as: :value do |stage|
+ stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil
+ end
+end
diff --git a/app/serializers/analytics_stage_serializer.rb b/app/serializers/analytics_stage_serializer.rb
new file mode 100644
index 00000000000..613cf6874d8
--- /dev/null
+++ b/app/serializers/analytics_stage_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsStageSerializer < BaseSerializer
+ entity AnalyticsStageEntity
+end
diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb
new file mode 100644
index 00000000000..91803ec07f5
--- /dev/null
+++ b/app/serializers/analytics_summary_entity.rb
@@ -0,0 +1,7 @@
+class AnalyticsSummaryEntity < Grape::Entity
+ expose :value, safe: true
+
+ expose :title do |object|
+ object.title.pluralize(object.value)
+ end
+end
diff --git a/app/serializers/analytics_summary_serializer.rb b/app/serializers/analytics_summary_serializer.rb
new file mode 100644
index 00000000000..c87a24aa47c
--- /dev/null
+++ b/app/serializers/analytics_summary_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsSummarySerializer < BaseSerializer
+ entity AnalyticsSummaryEntity
+end
diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb
new file mode 100644
index 00000000000..184f5fd4b52
--- /dev/null
+++ b/app/serializers/build_action_entity.rb
@@ -0,0 +1,14 @@
+class BuildActionEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name do |build|
+ build.name
+ end
+
+ expose :path do |build|
+ play_namespace_project_build_path(
+ build.project.namespace,
+ build.project,
+ build)
+ end
+end
diff --git a/app/serializers/build_artifact_entity.rb b/app/serializers/build_artifact_entity.rb
new file mode 100644
index 00000000000..8b643d8e783
--- /dev/null
+++ b/app/serializers/build_artifact_entity.rb
@@ -0,0 +1,14 @@
+class BuildArtifactEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name do |build|
+ build.name
+ end
+
+ expose :path do |build|
+ download_namespace_project_build_artifacts_path(
+ build.project.namespace,
+ build.project,
+ build)
+ end
+end
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index 3d9ac66de0e..b5384e6462b 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -4,21 +4,24 @@ class BuildEntity < Grape::Entity
expose :id
expose :name
- expose :build_url do |build|
- url_to(:namespace_project_build, build)
+ expose :build_path do |build|
+ path_to(:namespace_project_build, build)
end
- expose :retry_url do |build|
- url_to(:retry_namespace_project_build, build)
+ expose :retry_path do |build|
+ path_to(:retry_namespace_project_build, build)
end
- expose :play_url, if: ->(build, _) { build.manual? } do |build|
- url_to(:play_namespace_project_build, build)
+ expose :play_path, if: ->(build, _) { build.manual? } do |build|
+ path_to(:play_namespace_project_build, build)
end
+ expose :created_at
+ expose :updated_at
+
private
- def url_to(route, build)
- send("#{route}_url", build.project.namespace, build.project, build)
+ def path_to(route, build)
+ send("#{route}_path", build.project.namespace, build.project, build)
end
end
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
index f7eba6fc1e3..31763955f97 100644
--- a/app/serializers/commit_entity.rb
+++ b/app/serializers/commit_entity.rb
@@ -3,10 +3,21 @@ class CommitEntity < API::Entities::RepoCommit
expose :author, using: UserEntity
+ expose :author_gravatar_url do |commit|
+ GravatarService.new.execute(commit.author_email)
+ end
+
expose :commit_url do |commit|
- namespace_project_tree_url(
+ namespace_project_commit_url(
+ request.project.namespace,
+ request.project,
+ commit)
+ end
+
+ expose :commit_path do |commit|
+ namespace_project_commit_path(
request.project.namespace,
request.project,
- id: commit.id)
+ commit)
end
end
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index ad6fc8d665b..d610fbe0c8a 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -10,8 +10,8 @@ class DeploymentEntity < Grape::Entity
deployment.ref
end
- expose :ref_url do |deployment|
- namespace_project_tree_url(
+ expose :ref_path do |deployment|
+ namespace_project_tree_path(
deployment.project.namespace,
deployment.project,
id: deployment.ref)
diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb
new file mode 100644
index 00000000000..9607ad55a8b
--- /dev/null
+++ b/app/serializers/entity_date_helper.rb
@@ -0,0 +1,37 @@
+module EntityDateHelper
+ include ActionView::Helpers::DateHelper
+
+ def interval_in_words(diff)
+ return 'Not started' unless diff
+
+ "#{distance_of_time_in_words(Time.now, diff)} ago"
+ end
+
+ # Converts seconds into a hash such as:
+ # { days: 1, hours: 3, mins: 42, seconds: 40 }
+ #
+ # It returns 0 seconds for zero or negative numbers
+ # It rounds to nearest time unit and does not return zero
+ # i.e { min: 1 } instead of { mins: 1, seconds: 0 }
+ def distance_of_time_as_hash(diff)
+ diff = diff.abs.floor
+
+ return { seconds: 0 } if diff == 0
+
+ mins = (diff / 60).floor
+ seconds = diff % 60
+ hours = (mins / 60).floor
+ mins = mins % 60
+ days = (hours / 24).floor
+ hours = hours % 24
+
+ duration_hash = {}
+
+ duration_hash[:days] = days if days > 0
+ duration_hash[:hours] = hours if hours > 0
+ duration_hash[:mins] = mins if mins > 0
+ duration_hash[:seconds] = seconds if seconds > 0
+
+ duration_hash
+ end
+end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index bfccfd8bb7c..7279de59aa8 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -9,12 +9,27 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity
expose :can_run_stop_action?
- expose :environment_url do |environment|
- namespace_project_environment_url(
+ expose :environment_path do |environment|
+ namespace_project_environment_path(
environment.project.namespace,
environment.project,
environment)
end
+ expose :stop_path do |environment|
+ stop_namespace_project_environment_path(
+ environment.project.namespace,
+ environment.project,
+ environment)
+ end
+
+ expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment|
+ can?(request.user, :admin_environment, environment.project) &&
+ terminal_namespace_project_environment_path(
+ environment.project.namespace,
+ environment.project,
+ environment)
+ end
+
expose :created_at, :updated_at
end
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
new file mode 100644
index 00000000000..29aecb50849
--- /dev/null
+++ b/app/serializers/issuable_entity.rb
@@ -0,0 +1,20 @@
+class IssuableEntity < Grape::Entity
+ expose :id
+ expose :iid
+ expose :assignee_id
+ expose :author_id
+ expose :description
+ expose :lock_version
+ expose :milestone_id
+ expose :position
+ expose :state
+ expose :title
+ expose :updated_by_id
+ expose :created_at
+ expose :updated_at
+ expose :deleted_at
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
new file mode 100644
index 00000000000..6429159ebe1
--- /dev/null
+++ b/app/serializers/issue_entity.rb
@@ -0,0 +1,9 @@
+class IssueEntity < IssuableEntity
+ expose :branch_name
+ expose :confidential
+ expose :due_date
+ expose :moved_to_id
+ expose :project_id
+ expose :milestone, using: API::Entities::Milestone
+ expose :labels, using: LabelEntity
+end
diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb
new file mode 100644
index 00000000000..4fff54a9126
--- /dev/null
+++ b/app/serializers/issue_serializer.rb
@@ -0,0 +1,3 @@
+class IssueSerializer < BaseSerializer
+ entity IssueEntity
+end
diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb
new file mode 100644
index 00000000000..304fd9de08f
--- /dev/null
+++ b/app/serializers/label_entity.rb
@@ -0,0 +1,11 @@
+class LabelEntity < Grape::Entity
+ expose :id
+ expose :title
+ expose :color
+ expose :description
+ expose :group_id
+ expose :project_id
+ expose :template
+ expose :created_at
+ expose :updated_at
+end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
new file mode 100644
index 00000000000..7445298c714
--- /dev/null
+++ b/app/serializers/merge_request_entity.rb
@@ -0,0 +1,14 @@
+class MergeRequestEntity < IssuableEntity
+ expose :in_progress_merge_commit_sha
+ expose :locked_at
+ expose :merge_commit_sha
+ expose :merge_error
+ expose :merge_params
+ expose :merge_status
+ expose :merge_user_id
+ expose :merge_when_build_succeeds
+ expose :source_branch
+ expose :source_project_id
+ expose :target_branch
+ expose :target_project_id
+end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
new file mode 100644
index 00000000000..aa6e00dfcb4
--- /dev/null
+++ b/app/serializers/merge_request_serializer.rb
@@ -0,0 +1,3 @@
+class MergeRequestSerializer < BaseSerializer
+ entity MergeRequestEntity
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
new file mode 100644
index 00000000000..61f0f11d7d2
--- /dev/null
+++ b/app/serializers/pipeline_entity.rb
@@ -0,0 +1,85 @@
+class PipelineEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :user, using: UserEntity
+
+ expose :path do |pipeline|
+ namespace_project_pipeline_path(
+ pipeline.project.namespace,
+ pipeline.project,
+ pipeline)
+ end
+
+ expose :details do
+ expose :status do |pipeline, options|
+ StatusEntity.represent(
+ pipeline.detailed_status(request.user),
+ options)
+ end
+
+ expose :duration
+ expose :finished_at
+ expose :stages, using: StageEntity
+ expose :artifacts, using: BuildArtifactEntity
+ expose :manual_actions, using: BuildActionEntity
+ end
+
+ expose :flags do
+ expose :latest?, as: :latest
+ expose :triggered?, as: :triggered
+ expose :stuck?, as: :stuck
+ expose :has_yaml_errors?, as: :yaml_errors
+ expose :can_retry?, as: :retryable
+ expose :can_cancel?, as: :cancelable
+ end
+
+ expose :ref do
+ expose :name do |pipeline|
+ pipeline.ref
+ end
+
+ expose :path do |pipeline|
+ if pipeline.ref
+ namespace_project_tree_path(
+ pipeline.project.namespace,
+ pipeline.project,
+ id: pipeline.ref)
+ end
+ end
+
+ expose :tag?, as: :tag
+ expose :branch?, as: :branch
+ end
+
+ expose :commit, using: CommitEntity
+ expose :yaml_errors, if: ->(pipeline, _) { pipeline.has_yaml_errors? }
+
+ expose :retry_path, if: proc { can_retry? } do |pipeline|
+ retry_namespace_project_pipeline_path(pipeline.project.namespace,
+ pipeline.project,
+ pipeline.id)
+ end
+
+ expose :cancel_path, if: proc { can_cancel? } do |pipeline|
+ cancel_namespace_project_pipeline_path(pipeline.project.namespace,
+ pipeline.project,
+ pipeline.id)
+ end
+
+ expose :created_at, :updated_at
+
+ private
+
+ alias_method :pipeline, :object
+
+ def can_retry?
+ pipeline.retryable? &&
+ can?(request.user, :update_pipeline, pipeline)
+ end
+
+ def can_cancel?
+ pipeline.cancelable? &&
+ can?(request.user, :update_pipeline, pipeline)
+ end
+end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
new file mode 100644
index 00000000000..cfa86cc2553
--- /dev/null
+++ b/app/serializers/pipeline_serializer.rb
@@ -0,0 +1,40 @@
+class PipelineSerializer < BaseSerializer
+ entity PipelineEntity
+ class InvalidResourceError < StandardError; end
+ include API::Helpers::Pagination
+ Struct.new('Pagination', :request, :response)
+
+ def represent(resource, opts = {})
+ if paginated?
+ raise InvalidResourceError unless resource.respond_to?(:page)
+
+ super(paginate(resource.includes(project: :namespace)), opts)
+ else
+ super(resource, opts)
+ end
+ end
+
+ def paginated?
+ defined?(@pagination)
+ end
+
+ def with_pagination(request, response)
+ tap { @pagination = Struct::Pagination.new(request, response) }
+ end
+
+ private
+
+ # Methods needed by `API::Helpers::Pagination`
+ #
+ def params
+ @pagination.request.query_parameters
+ end
+
+ def request
+ @pagination.request
+ end
+
+ def header(header, value)
+ @pagination.response.headers[header] = value
+ end
+end
diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb
index ff8c1142abc..3039014aaaa 100644
--- a/app/serializers/request_aware_entity.rb
+++ b/app/serializers/request_aware_entity.rb
@@ -2,10 +2,11 @@ module RequestAwareEntity
extend ActiveSupport::Concern
included do
- include Gitlab::Routing.url_helpers
+ include Gitlab::Routing
+ include Gitlab::Allowable
end
def request
- @options.fetch(:request)
+ options.fetch(:request)
end
end
diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb
new file mode 100644
index 00000000000..7a047bdc712
--- /dev/null
+++ b/app/serializers/stage_entity.rb
@@ -0,0 +1,38 @@
+class StageEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name
+
+ expose :title do |stage|
+ "#{stage.name}: #{detailed_status.label}"
+ end
+
+ expose :detailed_status,
+ as: :status,
+ with: StatusEntity
+
+ expose :path do |stage|
+ namespace_project_pipeline_path(
+ stage.pipeline.project.namespace,
+ stage.pipeline.project,
+ stage.pipeline,
+ anchor: stage.name)
+ end
+
+ expose :dropdown_path do |stage|
+ stage_namespace_project_pipeline_path(
+ stage.pipeline.project.namespace,
+ stage.pipeline.project,
+ stage.pipeline,
+ stage: stage.name,
+ format: :json)
+ end
+
+ private
+
+ alias_method :stage, :object
+
+ def detailed_status
+ stage.detailed_status(request.user)
+ end
+end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
new file mode 100644
index 00000000000..47066bebfb1
--- /dev/null
+++ b/app/serializers/status_entity.rb
@@ -0,0 +1,8 @@
+class StatusEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :icon, :text, :label, :group
+
+ expose :has_details?, as: :has_details
+ expose :details_path
+end
diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
new file mode 100644
index 00000000000..ddaaed90e5b
--- /dev/null
+++ b/app/services/access_token_validation_service.rb
@@ -0,0 +1,32 @@
+AccessTokenValidationService = Struct.new(:token) do
+ # Results:
+ VALID = :valid
+ EXPIRED = :expired
+ REVOKED = :revoked
+ INSUFFICIENT_SCOPE = :insufficient_scope
+
+ def validate(scopes: [])
+ if token.expired?
+ return EXPIRED
+
+ elsif token.revoked?
+ return REVOKED
+
+ elsif !self.include_any_scope?(scopes)
+ return INSUFFICIENT_SCOPE
+
+ else
+ return VALID
+ end
+ end
+
+ # True if the token's scope contains any of the passed scopes.
+ def include_any_scope?(scopes)
+ if scopes.blank?
+ true
+ else
+ # Check whether the token is allowed access to any of the required scopes.
+ Set.new(scopes).intersection(Set.new(token.scopes)).present?
+ end
+ end
+end
diff --git a/app/services/after_branch_delete_service.rb b/app/services/after_branch_delete_service.rb
new file mode 100644
index 00000000000..227e9ea9c6d
--- /dev/null
+++ b/app/services/after_branch_delete_service.rb
@@ -0,0 +1,21 @@
+##
+# Branch can be deleted either by DeleteBranchService
+# or by GitPushService.
+#
+class AfterBranchDeleteService < BaseService
+ attr_reader :branch_name
+
+ def execute(branch_name)
+ @branch_name = branch_name
+
+ stop_environments
+ end
+
+ private
+
+ def stop_environments
+ Ci::StopEnvironmentsService
+ .new(project, current_user)
+ .execute(branch_name)
+ end
+end
diff --git a/app/services/chat_names/authorize_user_service.rb b/app/services/chat_names/authorize_user_service.rb
new file mode 100644
index 00000000000..321bf3a9205
--- /dev/null
+++ b/app/services/chat_names/authorize_user_service.rb
@@ -0,0 +1,38 @@
+module ChatNames
+ class AuthorizeUserService
+ include Gitlab::Routing.url_helpers
+
+ def initialize(service, params)
+ @service = service
+ @params = params
+ end
+
+ def execute
+ return unless chat_name_params.values.all?(&:present?)
+
+ token = request_token
+
+ new_profile_chat_name_url(token: token) if token
+ end
+
+ private
+
+ def request_token
+ chat_name_token.store!(chat_name_params)
+ end
+
+ def chat_name_token
+ Gitlab::ChatNameToken.new
+ end
+
+ def chat_name_params
+ {
+ service_id: @service.id,
+ team_id: @params[:team_id],
+ team_domain: @params[:team_domain],
+ chat_id: @params[:user_id],
+ chat_name: @params[:user_name]
+ }
+ end
+ end
+end
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb
new file mode 100644
index 00000000000..4f5c5567b42
--- /dev/null
+++ b/app/services/chat_names/find_user_service.rb
@@ -0,0 +1,26 @@
+module ChatNames
+ class FindUserService
+ def initialize(service, params)
+ @service = service
+ @params = params
+ end
+
+ def execute
+ chat_name = find_chat_name
+ return unless chat_name
+
+ chat_name.touch(:last_used_at)
+ chat_name.user
+ end
+
+ private
+
+ def find_chat_name
+ ChatName.find_by(
+ service: @service,
+ team_id: @params[:team_id],
+ chat_id: @params[:user_id]
+ )
+ end
+ end
+end
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
index 005014fa1de..b7da3f8e7eb 100644
--- a/app/services/ci/create_pipeline_builds_service.rb
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -10,18 +10,29 @@ module Ci
end
end
+ def project
+ pipeline.project
+ end
+
private
def create_build(build_attributes)
build_attributes = build_attributes.merge(
pipeline: pipeline,
- project: pipeline.project,
+ project: project,
ref: pipeline.ref,
tag: pipeline.tag,
user: current_user,
trigger_request: trigger_request
)
- pipeline.builds.create(build_attributes)
+ build = pipeline.builds.create(build_attributes)
+
+ # Create the environment before the build starts. This sets its slug and
+ # makes it available as an environment variable
+ project.environments.find_or_create_by(name: build.expanded_environment_name) if
+ build.has_environment?
+
+ build
end
def new_builds
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index cde856b0186..e3bc9847200 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -45,9 +45,15 @@ module Ci
return error('No builds for this pipeline.')
end
- pipeline.save
- pipeline.process!
- pipeline
+ Ci::Pipeline.transaction do
+ pipeline.save
+
+ Ci::CreatePipelineBuildsService
+ .new(project, current_user)
+ .execute(pipeline)
+ end
+
+ pipeline.tap(&:process!)
end
private
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 75d847d5bee..240ddabec36 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,13 +1,13 @@
module Ci
class ImageForBuildService
def execute(project, opts)
- sha = opts[:sha] || ref_sha(project, opts[:ref])
-
+ ref = opts[:ref]
+ sha = opts[:sha] || ref_sha(project, ref)
pipelines = project.pipelines.where(sha: sha)
- pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref]
- image_name = image_for_status(pipelines.status)
+ image_name = image_for_status(pipelines.latest_status(ref))
image_path = Rails.root.join('public/ci', image_name)
+
OpenStruct.new(path: image_path, name: image_name)
end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 8face432d97..79eb97b7b55 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -5,10 +5,7 @@ module Ci
def execute(pipeline)
@pipeline = pipeline
- # This method will ensure that our pipeline does have all builds for all stages created
- if created_builds.empty?
- create_builds!
- end
+ ensure_created_builds! # TODO, remove me in 9.0
new_builds =
stage_indexes_of_created_builds.map do |index|
@@ -22,10 +19,6 @@ module Ci
private
- def create_builds!
- Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline)
- end
-
def process_stage(index)
current_status = status_for_prior_stages(index)
@@ -51,11 +44,11 @@ module Ci
def valid_statuses_for_when(value)
case value
when 'on_success'
- %w[success]
+ %w[success skipped]
when 'on_failure'
%w[failed]
when 'always'
- %w[success failed]
+ %w[success failed skipped]
else
[]
end
@@ -76,5 +69,18 @@ module Ci
def created_builds
pipeline.builds.created
end
+
+ # This method is DEPRECATED and should be removed in 9.0.
+ #
+ # We need it to maintain backwards compatibility with previous versions
+ # when builds were not created within one transaction with the pipeline.
+ #
+ def ensure_created_builds!
+ return if created_builds.any?
+
+ Ci::CreatePipelineBuildsService
+ .new(project, current_user)
+ .execute(pipeline)
+ end
end
end
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
new file mode 100644
index 00000000000..cf590459cb2
--- /dev/null
+++ b/app/services/ci/stop_environments_service.rb
@@ -0,0 +1,29 @@
+module Ci
+ class StopEnvironmentsService < BaseService
+ attr_reader :ref
+
+ def execute(branch_name)
+ @ref = branch_name
+
+ return unless has_ref?
+
+ environments.each do |environment|
+ next unless environment.stoppable?
+ next unless can?(current_user, :create_deployment, project)
+
+ environment.stop!(current_user)
+ end
+ end
+
+ private
+
+ def has_ref?
+ @ref.present?
+ end
+
+ def environments
+ @environments ||= project
+ .environments_recently_updated_on_branch(@ref)
+ end
+ end
+end
diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb
new file mode 100644
index 00000000000..152c8ae5006
--- /dev/null
+++ b/app/services/ci/update_build_queue_service.rb
@@ -0,0 +1,19 @@
+module Ci
+ class UpdateBuildQueueService
+ def execute(build)
+ build.project.runners.each do |runner|
+ if runner.can_pick?(build)
+ runner.tick_runner_queue
+ end
+ end
+
+ return unless build.project.shared_runners_enabled?
+
+ Ci::Runner.shared.each do |runner|
+ if runner.can_pick?(build)
+ runner.tick_runner_queue
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 1c82599c579..4d410f66c55 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -34,8 +34,8 @@ module Commits
repository.public_send(action, current_user, @commit, into, tree_id)
success
else
- error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically.
- It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content."
+ error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
+ A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg
end
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 757fc35a78f..e004a303496 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class CreateBranchService < BaseService
def execute(branch_name, ref, source_project: @project)
valid_branch = Gitlab::GitRefValidator.validate(branch_name)
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index 8ae15ad32f4..47f9b2c621c 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class CreateDeploymentService < BaseService
def execute(deployable = nil)
return unless executable?
diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb
index d6d4afcf29a..54ff1f74126 100644
--- a/app/services/create_release_service.rb
+++ b/app/services/create_release_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class CreateReleaseService < BaseService
def execute(tag_name, release_description)
repository = project.repository
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index c0e7ecf6a96..fe9353afeb8 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class CreateTagService < BaseService
def execute(tag_name, target, message, release_description = nil)
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 3e5dd4ebb86..11a045f4c31 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class DeleteBranchService < BaseService
def execute(branch_name)
repository = project.repository
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
new file mode 100644
index 00000000000..1b5623baebe
--- /dev/null
+++ b/app/services/delete_merged_branches_service.rb
@@ -0,0 +1,16 @@
+class DeleteMergedBranchesService < BaseService
+ def async_execute
+ DeleteMergedBranchesWorker.perform_async(project.id, current_user.id)
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
+
+ branches = project.repository.branch_names
+ branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
+
+ branches.each do |branch|
+ DeleteBranchService.new(project, current_user).execute(branch)
+ end
+ end
+end
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index d824406cb49..a44dee14a0f 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class DeleteTagService < BaseService
def execute(tag_name)
repository = project.repository
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index 0081364b8aa..2316c57bf1e 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -6,12 +6,10 @@ class DestroyGroupService
end
def async_execute
- group.transaction do
- # Soft delete via paranoia gem
- group.destroy
- job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
- Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
- end
+ # Soft delete via paranoia gem
+ group.destroy
+ job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
+ Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
end
def execute
@@ -22,6 +20,10 @@ class DestroyGroupService
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end
+ group.children.each do |group|
+ DestroyGroupService.new(group, current_user).async_execute
+ end
+
group.really_destroy!
end
end
diff --git a/app/services/discussions/base_service.rb b/app/services/discussions/base_service.rb
new file mode 100644
index 00000000000..e4dfe6e71bb
--- /dev/null
+++ b/app/services/discussions/base_service.rb
@@ -0,0 +1,4 @@
+module Discussions
+ class BaseService < ::BaseService
+ end
+end
diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb
new file mode 100644
index 00000000000..0437195f588
--- /dev/null
+++ b/app/services/discussions/resolve_service.rb
@@ -0,0 +1,24 @@
+module Discussions
+ class ResolveService < Discussions::BaseService
+ def execute(one_or_more_discussions)
+ Array(one_or_more_discussions).each { |discussion| resolve_discussion(discussion) }
+ end
+
+ def resolve_discussion(discussion)
+ return unless discussion.can_resolve?(current_user)
+
+ discussion.resolve!(current_user)
+
+ MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
+ SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue
+ end
+
+ def merge_request
+ params[:merge_request]
+ end
+
+ def follow_up_issue
+ params[:follow_up_issue]
+ end
+ end
+end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index d00d78cee7e..e5b4d60e467 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -1,5 +1,3 @@
-require_relative "base_service"
-
module Files
class CreateDirService < Files::BaseService
def commit
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index bf127843d55..b23576b9a28 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,5 +1,3 @@
-require_relative "base_service"
-
module Files
class CreateService < Files::BaseService
def commit
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
index 8b27ad51789..4f7e7a5baaa 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -1,5 +1,3 @@
-require_relative "base_service"
-
module Files
class DeleteService < Files::BaseService
def commit
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index d28912e1301..54446e90007 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,5 +1,3 @@
-require_relative "base_service"
-
module Files
class MultiService < Files::BaseService
class FileChangedError < StandardError; end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index c17fdb8d1f1..47a18e3e132 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -1,5 +1,3 @@
-require_relative "base_service"
-
module Files
class UpdateService < Files::BaseService
class FileChangedError < StandardError; end
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
index 172bd85dade..6cd3908d43a 100644
--- a/app/services/git_hooks_service.rb
+++ b/app/services/git_hooks_service.rb
@@ -1,6 +1,8 @@
class GitHooksService
PreReceiveError = Class.new(StandardError)
+ attr_accessor :oldrev, :newrev, :ref
+
def execute(user, repo_path, oldrev, newrev, ref)
@repo_path = repo_path
@user = Gitlab::GlId.gl_id(user)
@@ -16,7 +18,7 @@ class GitHooksService
end
end
- yield
+ yield self
run_hook('post-receive')
end
@@ -25,6 +27,6 @@ class GitHooksService
def run_hook(name)
hook = Gitlab::Git::Hook.new(name, @repo_path)
- hook.trigger(@user, @oldrev, @newrev, @ref)
+ hook.trigger(@user, oldrev, newrev, ref)
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index de313095bed..dbe2fda27b5 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -3,6 +3,9 @@ class GitPushService < BaseService
include Gitlab::CurrentSettings
include Gitlab::Access
+ # The N most recent commits to process in a single push payload.
+ PROCESS_COMMIT_LIMIT = 100
+
# This method will be called after each git update
# and only if the provided user and project are present in GitLab.
#
@@ -18,7 +21,7 @@ class GitPushService < BaseService
#
def execute
@project.repository.after_create if @project.empty_repo?
- @project.repository.after_push_commit(branch_name, params[:newrev])
+ @project.repository.after_push_commit(branch_name)
if push_remove_branch?
@project.repository.after_remove_branch
@@ -49,27 +52,63 @@ class GitPushService < BaseService
update_gitattributes if is_default_branch?
end
- # Update merge requests that may be affected by this push. A new branch
- # could cause the last commit of a merge request to change.
- update_merge_requests
-
+ execute_related_hooks
perform_housekeeping
+
+ update_caches
end
def update_gitattributes
@project.repository.copy_gitattributes(params[:ref])
end
+ def update_caches
+ if is_default_branch?
+ paths = Set.new
+
+ @push_commits.each do |commit|
+ commit.raw_diffs(deltas_only: true).each do |diff|
+ paths << diff.new_path
+ end
+ end
+
+ types = Gitlab::FileDetector.types_in_paths(paths.to_a)
+ else
+ types = []
+ end
+
+ ProjectCacheWorker.perform_async(@project.id, types, [:commit_count, :repository_size])
+ end
+
+ # Schedules processing of commit messages.
+ def process_commit_messages
+ default = is_default_branch?
+
+ push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit|
+ ProcessCommitWorker.
+ perform_async(project.id, current_user.id, commit.to_hash, default)
+ end
+ end
+
protected
- def update_merge_requests
- UpdateMergeRequestsWorker.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
+ def execute_related_hooks
+ # Update merge requests that may be affected by this push. A new branch
+ # could cause the last commit of a merge request to change.
+ #
+ UpdateMergeRequestsWorker
+ .perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
EventCreateService.new.push(@project, current_user, build_push_data)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute
- ProjectCacheWorker.perform_async(@project.id)
+
+ if push_remove_branch?
+ AfterBranchDeleteService
+ .new(project, current_user)
+ .execute(branch_name)
+ end
end
def perform_housekeeping
@@ -102,17 +141,6 @@ class GitPushService < BaseService
end
end
- # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
- # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
- def process_commit_messages
- default = is_default_branch?
-
- @push_commits.each do |commit|
- ProcessCommitWorker.
- perform_async(project.id, current_user.id, commit.id, default)
- end
- end
-
def build_push_data
@push_data ||= Gitlab::DataBuilder::Push.build(
@project,
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 20a4445bddf..96432837481 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -12,7 +12,7 @@ class GitTagPushService < BaseService
project.execute_hooks(@push_data.dup, :tag_push_hooks)
project.execute_services(@push_data.dup, :tag_push_hooks)
Ci::CreatePipelineService.new(project, current_user, @push_data).execute
- ProjectCacheWorker.perform_async(project.id)
+ ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size])
true
end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index 2bccd584dde..febeb661fb5 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -12,6 +12,13 @@ module Groups
return @group
end
+ if @group.parent && !can?(current_user, :admin_group, @group.parent)
+ @group.parent = nil
+ @group.errors.add(:parent_id, 'manage access required to create subgroup')
+
+ return @group
+ end
+
@group.name ||= @group.path.dup
@group.save
@group.add_owner(current_user)
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 99ad12b1003..4e878ec556a 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -5,7 +5,7 @@ module Groups
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != group.visibility_level
unless can?(current_user, :change_visibility_level, group) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(group, new_visibility)
return group
@@ -14,7 +14,13 @@ module Groups
group.assign_attributes(params)
- group.save
+ begin
+ group.save
+ rescue Gitlab::UpdatePathError => e
+ group.errors.add(:base, e.message)
+
+ false
+ end
end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index bb92cd80cc9..5f3ced49665 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -36,14 +36,18 @@ class IssuableBaseService < BaseService
end
end
- def filter_params(issuable_ability_name = :issue)
- filter_assignee
- filter_milestone
- filter_labels
+ def create_time_estimate_note(issuable)
+ SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
+ end
- ability = :"admin_#{issuable_ability_name}"
+ def create_time_spent_note(issuable)
+ SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
+ end
- unless can?(current_user, ability, project)
+ def filter_params(issuable)
+ ability_name = :"admin_#{issuable.to_ability_name}"
+
+ unless can?(current_user, ability_name, project)
params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
@@ -52,14 +56,35 @@ class IssuableBaseService < BaseService
params.delete(:assignee_id)
params.delete(:due_date)
end
+
+ filter_assignee(issuable)
+ filter_milestone
+ filter_labels
end
- def filter_assignee
- if params[:assignee_id] == IssuableFinder::NONE
- params[:assignee_id] = ''
+ def filter_assignee(issuable)
+ return unless params[:assignee_id].present?
+
+ assignee_id = params[:assignee_id]
+
+ if assignee_id.to_s == IssuableFinder::NONE
+ params[:assignee_id] = ""
+ else
+ params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id)
end
end
+ def assignee_can_read?(issuable, assignee_id)
+ new_assignee = User.find_by_id(assignee_id)
+
+ return false unless new_assignee.present?
+
+ ability_name = :"read_#{issuable.to_ability_name}"
+ resource = issuable.persisted? ? issuable : project
+
+ can?(new_assignee, ability_name, resource)
+ end
+
def filter_milestone
milestone_id = params[:milestone_id]
return unless milestone_id
@@ -85,14 +110,15 @@ class IssuableBaseService < BaseService
def find_or_create_label_ids
labels = params.delete(:labels)
+
return unless labels
- params[:label_ids] = labels.split(',').map do |label_name|
+ params[:label_ids] = labels.split(",").map do |label_name|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
label = service.execute
- label.id
- end
+ label.try(:id)
+ end.compact
end
def process_label_ids(attributes, existing_label_ids: nil)
@@ -119,9 +145,10 @@ class IssuableBaseService < BaseService
def merge_slash_commands_into_params!(issuable)
description, command_params =
SlashCommands::InterpretService.new(project, current_user).
- execute(params[:description], issuable)
+ execute(params[:description], issuable)
- params[:description] = description
+ # Avoid a description already set on an issuable to be overwritten by a nil
+ params[:description] = description if params.has_key?(:description)
params.merge!(command_params)
end
@@ -136,10 +163,11 @@ class IssuableBaseService < BaseService
def create(issuable)
merge_slash_commands_into_params!(issuable)
- filter_params
+ filter_params(issuable)
params.delete(:state_event)
params[:author] ||= current_user
+
label_ids = process_label_ids(params)
issuable.assign_attributes(params)
@@ -177,15 +205,14 @@ class IssuableBaseService < BaseService
change_state(issuable)
change_subscription(issuable)
change_todo(issuable)
- filter_params
+ filter_params(issuable)
old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a
- params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids)
+ label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
+ params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
if params.present? && update_issuable(issuable, params)
- issuable.reset_events_cache
-
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
handle_common_system_notes(issuable, old_labels: old_labels)
@@ -200,6 +227,10 @@ class IssuableBaseService < BaseService
issuable
end
+ def labels_changing?(old_label_ids, new_label_ids)
+ old_label_ids.sort != new_label_ids.sort
+ end
+
def change_state(issuable)
case params.delete(:state_event)
when 'reopen'
@@ -212,9 +243,9 @@ class IssuableBaseService < BaseService
def change_subscription(issuable)
case params.delete(:subscription_event)
when 'subscribe'
- issuable.subscribe(current_user)
+ issuable.subscribe(current_user, project)
when 'unsubscribe'
- issuable.unsubscribe(current_user)
+ issuable.unsubscribe(current_user, project)
end
end
@@ -249,6 +280,14 @@ class IssuableBaseService < BaseService
create_task_status_note(issuable)
end
+ if issuable.previous_changes.include?('time_estimate')
+ create_time_estimate_note(issuable)
+ end
+
+ if issuable.time_spent?
+ create_time_spent_note(issuable)
+ end
+
create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end
end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 9ea3ce084ba..35af867a098 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -1,5 +1,13 @@
module Issues
class BaseService < ::IssuableBaseService
+ attr_reader :merge_request_for_resolving_discussions
+
+ def initialize(*args)
+ super
+
+ @merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions)
+ end
+
def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.build(issue)
@@ -9,10 +17,6 @@ module Issues
private
- def filter_params
- super(:issue)
- end
-
def execute_hooks(issue, action = 'open')
issue_data = hook_data(issue, action)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
new file mode 100644
index 00000000000..a63982f60c8
--- /dev/null
+++ b/app/services/issues/build_service.rb
@@ -0,0 +1,50 @@
+module Issues
+ class BuildService < Issues::BaseService
+ def execute
+ @issue = project.issues.new(issue_params)
+ end
+
+ def issue_params_with_info_from_merge_request
+ return {} unless merge_request_for_resolving_discussions
+
+ { title: title_from_merge_request, description: description_from_merge_request }
+ end
+
+ def title_from_merge_request
+ "Follow-up from \"#{merge_request_for_resolving_discussions.title}\""
+ end
+
+ def description_from_merge_request
+ if merge_request_for_resolving_discussions.resolvable_discussions.empty?
+ return "There are no unresolved discussions. "\
+ "Review the conversation in #{merge_request_for_resolving_discussions.to_reference}"
+ end
+
+ description = "The following discussions from #{merge_request_for_resolving_discussions.to_reference} should be addressed:"
+ [description, *items_for_discussions].join("\n\n")
+ end
+
+ def items_for_discussions
+ merge_request_for_resolving_discussions.resolvable_discussions.map { |discussion| item_for_discussion(discussion) }
+ end
+
+ def item_for_discussion(discussion)
+ first_note = discussion.first_note_to_resolve
+ other_note_count = discussion.notes.size - 1
+ creation_time = first_note.created_at.to_s(:medium)
+ note_url = Gitlab::UrlBuilder.build(first_note)
+
+ discussion_info = "- [ ] #{first_note.author.to_reference} commented in a discussion on [#{creation_time}](#{note_url}): "
+ discussion_info << " (+#{other_note_count} #{'comment'.pluralize(other_note_count)})" if other_note_count > 0
+
+ note_without_block_quotes = Banzai::Filter::BlockquoteFenceFilter.new(first_note.note).call
+ quote = ">>>\n#{note_without_block_quotes}\n>>>"
+
+ [discussion_info, quote].join("\n\n")
+ end
+
+ def issue_params
+ @issue_params ||= issue_params_with_info_from_merge_request.merge(params.slice(:title, :description))
+ end
+ end
+end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index ab4c51386a4..f1030912c68 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -17,7 +17,7 @@ module Issues
# allowed to close the given issue.
def close_issue(issue, commit: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active
- project.jira_service.execute(commit, issue)
+ project.jira_service.close_issue(commit, issue)
todo_service.close_issue(issue, current_user)
return issue
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index ea1690f3e38..d2eb46ac41b 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -4,7 +4,8 @@ module Issues
@request = params.delete(:request)
@api = params.delete(:api)
- @issue = project.issues.new
+ issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
+ @issue = BuildService.new(project, current_user, issue_attributes).execute
create(@issue)
end
@@ -18,6 +19,17 @@ module Issues
notification_service.new_issue(issuable, current_user)
todo_service.new_issue(issuable, current_user)
user_agent_detail_service.create
+
+ if merge_request_for_resolving_discussions.try(:discussions_can_be_resolved_by?, current_user)
+ resolve_discussions_in_merge_request(issuable)
+ end
+ end
+
+ def resolve_discussions_in_merge_request(issue)
+ Discussions::ResolveService.new(project, current_user,
+ merge_request: merge_request_for_resolving_discussions,
+ follow_up_issue: issue).
+ execute(merge_request_for_resolving_discussions.resolvable_discussions)
end
private
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a2111b3806b..78cbf94ec69 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -10,7 +10,7 @@ module Issues
end
if issue.previous_changes.include?('title') ||
- issue.previous_changes.include?('description')
+ issue.previous_changes.include?('description')
todo_service.update_issue(issue, current_user)
end
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index d622f9edd33..cf4f7606c94 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -22,9 +22,14 @@ module Labels
).execute(skip_authorization: skip_authorization)
end
+ # Only creates the label if current_user can do so, if the label does not exist
+ # and the user can not create the label, nil is returned
def find_or_create_label
new_label = available_labels.find_by(title: title)
- new_label ||= project.labels.create(params)
+
+ if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project))
+ new_label = project.labels.create(params)
+ end
new_label
end
diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
index d572a928a42..12a8415d9a5 100644
--- a/app/services/merge_requests/add_todo_when_build_fails_service.rb
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -1,13 +1,18 @@
module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails
+ #
def execute(commit_status)
+ return if commit_status.allow_failure?
+
commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
end
end
- # Closes any pending build failed todos for the parent MRs when a build is retried
+ # Closes any pending build failed todos for the parent MRs when a
+ # build is retried
+ #
def close(commit_status)
commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 58f69a41e14..5a53b973059 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -38,24 +38,18 @@ module MergeRequests
private
- def filter_params
- super(:merge_request)
- end
-
- def merge_requests_for(branch)
- origin_merge_requests = @project.origin_merge_requests
- .opened.where(source_branch: branch).to_a
-
- fork_merge_requests = @project.fork_merge_requests
- .opened.where(source_branch: branch).to_a
-
- (origin_merge_requests + fork_merge_requests)
- .uniq.select(&:source_project)
+ # Returns all origin and fork merge requests from `@project` satisfying passed arguments.
+ def merge_requests_for(source_branch, mr_states: [:opened])
+ MergeRequest
+ .with_state(mr_states)
+ .where(source_branch: source_branch, source_project_id: @project.id)
+ .preload(:source_project) # we don't need a #includes since we're just preloading for the #select
+ .select(&:source_project)
end
def pipeline_merge_requests(pipeline)
merge_requests_for(pipeline.ref).each do |merge_request|
- next unless pipeline == merge_request.pipeline
+ next unless pipeline == merge_request.head_pipeline
yield merge_request
end
@@ -63,7 +57,7 @@ module MergeRequests
def commit_status_merge_requests(commit_status)
merge_requests_for(commit_status.ref).each do |merge_request|
- pipeline = merge_request.pipeline
+ pipeline = merge_request.head_pipeline
next unless pipeline
next unless pipeline.sha == commit_status.sha
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index f415244068b..6a7393a9921 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -42,17 +42,17 @@ module MergeRequests
end
if merge_request.source_project == merge_request.target_project &&
- merge_request.target_branch == merge_request.source_branch
+ merge_request.target_branch == merge_request.source_branch
messages << 'You must select different branches'
end
# See if source and target branches exist
- unless merge_request.source_project.commit(merge_request.source_branch)
+ if merge_request.source_branch.present? && !merge_request.source_project.commit(merge_request.source_branch)
messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
end
- unless merge_request.target_project.commit(merge_request.target_branch)
+ if merge_request.target_branch.present? && !merge_request.target_project.commit(merge_request.target_branch)
messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
end
@@ -81,7 +81,7 @@ module MergeRequests
commit = commits.first
merge_request.title = commit.title
merge_request.description ||= commit.description.try(:strip)
- elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?)
+ elsif iid && issue = merge_request.target_project.get_issue(iid, current_user)
case issue
when Issue
merge_request.title = "Resolve \"#{issue.title}\""
diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
index dc159de0058..5616edf8b4a 100644
--- a/app/services/merge_requests/merge_when_build_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
@@ -1,5 +1,5 @@
module MergeRequests
- class MergeWhenBuildSucceedsService < MergeRequests::BaseService
+ class MergeWhenPipelineSucceedsService < MergeRequests::BaseService
# Marks the passed `merge_request` to be merged when the build succeeds or
# updates the params for the automatic merge
def execute(merge_request)
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 22596b4014a..b4bfb0e5e8c 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -21,6 +21,7 @@ module MergeRequests
end
comment_mr_with_commits
+ mark_mr_as_wip_from_commits
execute_mr_web_hooks
true
@@ -41,7 +42,7 @@ module MergeRequests
commit_ids.include?(merge_request.diff_head_sha)
end
- merge_requests.uniq.select(&:source_project).each do |merge_request|
+ filter_merge_requests(merge_requests).each do |merge_request|
MergeRequests::PostMergeService.
new(merge_request.target_project, @current_user).
execute(merge_request)
@@ -55,15 +56,19 @@ module MergeRequests
# Refresh merge request diff if we push to source or target branch of merge request
# Note: we should update merge requests from forks too
def reload_merge_requests
- merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a
- merge_requests += fork_merge_requests.by_branch(@branch_name).to_a
- merge_requests = filter_merge_requests(merge_requests)
+ merge_requests = @project.merge_requests.opened.
+ by_source_or_target_branch(@branch_name).to_a
- merge_requests.each do |merge_request|
+ # Fork merge requests
+ merge_requests += MergeRequest.opened
+ .where(source_branch: @branch_name, source_project: @project)
+ .where.not(target_project: @project).to_a
+
+ filter_merge_requests(merge_requests).each do |merge_request|
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
- mr_commit_ids = merge_request.commits.map(&:id)
+ mr_commit_ids = merge_request.commits_sha
push_commit_ids = @commits.map(&:id)
matches = mr_commit_ids & push_commit_ids
merge_request.reload_diff if matches.any?
@@ -123,7 +128,7 @@ module MergeRequests
return unless @commits.present?
merge_requests_for_source_branch.each do |merge_request|
- mr_commit_ids = Set.new(merge_request.commits.map(&:id))
+ mr_commit_ids = Set.new(merge_request.commits_sha)
new_commits, existing_commits = @commits.partition do |commit|
mr_commit_ids.include?(commit.id)
@@ -135,6 +140,24 @@ module MergeRequests
end
end
+ def mark_mr_as_wip_from_commits
+ return unless @commits.present?
+
+ merge_requests_for_source_branch.each do |merge_request|
+ wip_commit = @commits.detect(&:work_in_progress?)
+
+ if wip_commit && !merge_request.work_in_progress?
+ merge_request.update(title: merge_request.wip_title)
+ SystemNoteService.add_merge_request_wip_from_commit(
+ merge_request,
+ merge_request.project,
+ @current_user,
+ wip_commit
+ )
+ end
+ end
+ end
+
# Call merge request webhook with update branches
def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request|
@@ -155,15 +178,7 @@ module MergeRequests
end
def merge_requests_for_source_branch
- @source_merge_requests ||= begin
- merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
- merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a
- filter_merge_requests(merge_requests)
- end
- end
-
- def fork_merge_requests
- @fork_merge_requests ||= @project.fork_merge_requests.opened
+ @source_merge_requests ||= merge_requests_for(@branch_name)
end
def branch_added?
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index a37cc3fdf21..3cb9aae83f6 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -1,7 +1,3 @@
-require_relative 'base_service'
-require_relative 'reopen_service'
-require_relative 'close_service'
-
module MergeRequests
class UpdateService < MergeRequests::BaseService
def execute(merge_request)
@@ -11,6 +7,8 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
+ merge_from_slash_command(merge_request) if params[:merge]
+
if merge_request.closed_without_fork?
params.except!(:target_branch, :force_remove_source_branch)
end
@@ -29,7 +27,7 @@ module MergeRequests
end
if merge_request.previous_changes.include?('title') ||
- merge_request.previous_changes.include?('description')
+ merge_request.previous_changes.include?('description')
todo_service.update_merge_request(merge_request, current_user)
end
@@ -73,6 +71,19 @@ module MergeRequests
end
end
+ def merge_from_slash_command(merge_request)
+ last_diff_sha = params.delete(:merge)
+ return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha)
+
+ merge_request.update(merge_error: nil)
+
+ if merge_request.head_pipeline && merge_request.head_pipeline.active?
+ MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request)
+ else
+ MergeWorker.perform_async(merge_request.id, current_user.id, {})
+ end
+ end
+
def reopen_service
MergeRequests::ReopenService
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 723cc0e6834..cdd765c85eb 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -1,6 +1,8 @@
module Notes
class CreateService < BaseService
def execute
+ merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
+
note = project.notes.new(params)
note.author = current_user
note.system = false
@@ -19,27 +21,33 @@ module Notes
slash_commands_service = SlashCommandsService.new(project, current_user)
if slash_commands_service.supported?(note)
- content, command_params = slash_commands_service.extract_commands(note)
+ options = { merge_request_diff_head_sha: merge_request_diff_head_sha }
+ content, command_params = slash_commands_service.extract_commands(note, options)
only_commands = content.empty?
note.note = content
end
- if !only_commands && note.save
+ note.run_after_commit do
# Finish the harder work in the background
- NewNoteWorker.perform_in(2.seconds, note.id, params)
+ NewNoteWorker.perform_async(note.id)
+ end
+
+ if !only_commands && note.save
todo_service.new_note(note, current_user)
end
- if command_params && command_params.any?
+ if command_params.present?
slash_commands_service.execute(command_params, note)
# We must add the error after we call #save because errors are reset
# when #save is called
if only_commands
- note.errors.add(:commands_only, 'Your commands have been executed!')
+ note.errors.add(:commands_only, 'Commands applied')
end
+
+ note.commands_changes = command_params.keys
end
note
diff --git a/app/services/notes/delete_service.rb b/app/services/notes/delete_service.rb
index 7f1b30ec84e..a673e8e9dde 100644
--- a/app/services/notes/delete_service.rb
+++ b/app/services/notes/delete_service.rb
@@ -2,7 +2,6 @@ module Notes
class DeleteService < BaseService
def execute(note)
note.destroy
- note.reset_events_cache
end
end
end
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index 2edbd39a9e7..aaea9717fc4 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -19,10 +19,10 @@ module Notes
self.class.supported?(note, current_user)
end
- def extract_commands(note)
+ def extract_commands(note, options = {})
return [note.note, {}] unless supported?(note)
- SlashCommands::InterpretService.new(project, current_user).
+ SlashCommands::InterpretService.new(project, current_user, options).
execute(note.note, note.noteable)
end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 1361b1e0300..75a4b3ed826 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -5,7 +5,6 @@ module Notes
note.update_attributes(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user)
- note.reset_events_cache
if note.previous_changes.include?('note')
TodoService.new.update_note(note, current_user)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 6697840cc26..c3b61e68eab 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -75,7 +75,7 @@ class NotificationService
# * watchers of the issue's labels
#
def relabeled_issue(issue, added_labels, current_user)
- relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
+ relabeled_resource_email(issue, issue.project, added_labels, current_user, :relabeled_issue_email)
end
# When create a merge request we should send an email to:
@@ -118,7 +118,7 @@ class NotificationService
# * watchers of the mr's labels
#
def relabeled_merge_request(merge_request, added_labels, current_user)
- relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email)
+ relabeled_resource_email(merge_request, merge_request.target_project, added_labels, current_user, :relabeled_merge_request_email)
end
def close_mr(merge_request, current_user)
@@ -171,7 +171,6 @@ class NotificationService
return true unless note.noteable_type.present?
# ignore gitlab service messages
- return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system?
target = note.noteable
@@ -205,7 +204,7 @@ class NotificationService
recipients = reject_muted_users(recipients, note.project)
- recipients = add_subscribed_users(recipients, note.noteable)
+ recipients = add_subscribed_users(recipients, note.project, note.noteable)
recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients = reject_users_without_access(recipients, note.noteable)
@@ -393,7 +392,7 @@ class NotificationService
)
end
- # Build a list of users based on project notifcation settings
+ # Build a list of users based on project notification settings
def select_project_member_setting(project, global_setting, users_global_level_watch)
users = notification_settings_for(project, :watch)
@@ -505,17 +504,17 @@ class NotificationService
end
end
- def add_subscribed_users(recipients, target)
+ def add_subscribed_users(recipients, project, target)
return recipients unless target.respond_to? :subscribers
- recipients + target.subscribers
+ recipients + target.subscribers(project)
end
- def add_labels_subscribers(recipients, target, labels: nil)
+ def add_labels_subscribers(recipients, project, target, labels: nil)
return recipients unless target.respond_to? :labels
(labels || target.labels).each do |label|
- recipients += label.subscribers
+ recipients += label.subscribers(project)
end
recipients
@@ -571,8 +570,8 @@ class NotificationService
end
end
- def relabeled_resource_email(target, labels, current_user, method)
- recipients = build_relabeled_recipients(target, current_user, labels: labels)
+ def relabeled_resource_email(target, project, labels, current_user, method)
+ recipients = build_relabeled_recipients(target, project, current_user, labels: labels)
label_names = labels.map(&:name)
recipients.each do |recipient|
@@ -592,7 +591,10 @@ class NotificationService
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
- recipients = add_project_watchers(recipients, project)
+
+ unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+ recipients = add_project_watchers(recipients, project)
+ end
recipients = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project)
@@ -608,10 +610,10 @@ class NotificationService
end
recipients = reject_muted_users(recipients, project)
- recipients = add_subscribed_users(recipients, target)
+ recipients = add_subscribed_users(recipients, project, target)
if [:new_issue, :new_merge_request].include?(custom_action)
- recipients = add_labels_subscribers(recipients, target)
+ recipients = add_labels_subscribers(recipients, project, target)
end
recipients = reject_unsubscribed_users(recipients, target)
@@ -622,8 +624,8 @@ class NotificationService
recipients.uniq
end
- def build_relabeled_recipients(target, current_user, labels:)
- recipients = add_labels_subscribers([], target, labels: labels)
+ def build_relabeled_recipients(target, project, current_user, labels:)
+ recipients = add_labels_subscribers([], project, target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user)
diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb
deleted file mode 100644
index 264fdccde8f..00000000000
--- a/app/services/oauth2/access_token_validation_service.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module Oauth2::AccessTokenValidationService
- # Results:
- VALID = :valid
- EXPIRED = :expired
- REVOKED = :revoked
- INSUFFICIENT_SCOPE = :insufficient_scope
-
- class << self
- def validate(token, scopes: [])
- if token.expired?
- return EXPIRED
-
- elsif token.revoked?
- return REVOKED
-
- elsif !self.sufficient_scope?(token, scopes)
- return INSUFFICIENT_SCOPE
-
- else
- return VALID
- end
- end
-
- protected
-
- # True if the token's scope is a superset of required scopes,
- # or the required scopes is empty.
- def sufficient_scope?(token, scopes)
- if scopes.blank?
- # if no any scopes required, the scopes of token is sufficient.
- return true
- else
- # If there are scopes required, then check whether
- # the set of authorized scopes is a superset of the set of required scopes
- required_scopes = Set.new(scopes)
- authorized_scopes = Set.new(token.scopes)
-
- return authorized_scopes >= required_scopes
- end
- end
- end
-end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 15d7918e7fd..159f46cd465 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -95,7 +95,7 @@ module Projects
unless @project.gitlab_project_import?
@project.create_wiki unless skip_wiki?
- @project.build_missing_services
+ create_services_from_active_templates(@project)
@project.create_labels
end
@@ -106,6 +106,8 @@ module Projects
unless @project.group || @project.gitlab_project_import?
@project.team << [current_user, :master, current_user]
end
+
+ @project.group.refresh_members_authorized_projects if @project.group
end
def skip_wiki?
@@ -135,5 +137,12 @@ module Projects
@project
end
+
+ def create_services_from_active_templates(project)
+ Service.where(template: true, active: true).each do |template|
+ service = Service.build_from_template(project.id, template)
+ service.save!
+ end
+ end
end
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index d7221fe993c..cd230528743 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -4,15 +4,6 @@ module Projects
class Error < StandardError; end
- ALLOWED_TYPES = [
- 'bitbucket',
- 'fogbugz',
- 'gitlab',
- 'github',
- 'google_code',
- 'gitlab_project'
- ]
-
def execute
add_repository_to_project unless project.gitlab_project_import?
@@ -64,14 +55,11 @@ module Projects
end
def has_importer?
- ALLOWED_TYPES.include?(project.import_type)
+ Gitlab::ImportSources.importer_names.include?(project.import_type)
end
def importer
- return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import?
-
- class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
- class_name.constantize.new(project)
+ Gitlab::ImportSources.importer(project.import_type).new(project)
end
def unknown_url?
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index d38328403c1..96c363c8d1a 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,7 +1,7 @@
module Projects
class ParticipantsService < BaseService
attr_reader :noteable
-
+
def execute(noteable)
@noteable = noteable
@@ -15,7 +15,8 @@ module Projects
[{
name: noteable.author.name,
- username: noteable.author.username
+ username: noteable.author.username,
+ avatar_url: noteable.author.avatar_url
}]
end
@@ -28,14 +29,14 @@ module Projects
def sorted(users)
users.uniq.to_a.compact.sort_by(&:username).map do |user|
- { username: user.username, name: user.name }
+ { username: user.username, name: user.name, avatar_url: user.avatar_url }
end
end
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
- { username: group.path, name: group.name, count: count }
+ { username: group.path, name: group.name, count: count, avatar_url: group.avatar_url }
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 28470f59807..34ec575e808 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -61,9 +61,6 @@ module Projects
# Move missing group labels to project
Labels::TransferService.new(current_user, old_group, project).execute
- # clear project cached events
- project.reset_events_cache
-
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 921ca6748d3..842e23eb6b6 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -6,10 +6,10 @@ module Projects
if new_visibility && new_visibility.to_i != project.visibility_level
unless can?(current_user, :change_visibility_level, project) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
- return project
+ return error('Visibility level unallowed')
end
end
@@ -23,6 +23,10 @@ module Projects
if project.previous_changes.include?('path')
project.rename_repo
end
+
+ success
+ else
+ error('Project could not be updated')
end
end
end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index 5a81194a5f4..3566a8ba92f 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -2,7 +2,7 @@ module SlashCommands
class InterpretService < BaseService
include Gitlab::SlashCommands::Dsl
- attr_reader :issuable
+ attr_reader :issuable, :options
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
@@ -13,7 +13,8 @@ module SlashCommands
opts = {
issuable: issuable,
current_user: current_user,
- project: project
+ project: project,
+ params: params
}
content, commands = extractor.extract_commands(content, opts)
@@ -58,6 +59,17 @@ module SlashCommands
@updates[:state_event] = 'reopen'
end
+ desc 'Merge (when build succeeds)'
+ condition do
+ last_diff_sha = params && params[:merge_request_diff_head_sha]
+ issuable.is_a?(MergeRequest) &&
+ issuable.persisted? &&
+ issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha)
+ end
+ command :merge do
+ @updates[:merge] = params[:merge_request_diff_head_sha]
+ end
+
desc 'Change title'
params '<New title>'
condition do
@@ -193,7 +205,7 @@ module SlashCommands
desc 'Subscribe'
condition do
issuable.persisted? &&
- !issuable.subscribed?(current_user)
+ !issuable.subscribed?(current_user, project)
end
command :subscribe do
@updates[:subscription_event] = 'subscribe'
@@ -202,7 +214,7 @@ module SlashCommands
desc 'Unsubscribe'
condition do
issuable.persisted? &&
- issuable.subscribed?(current_user)
+ issuable.subscribed?(current_user, project)
end
command :unsubscribe do
@updates[:subscription_event] = 'unsubscribe'
@@ -243,6 +255,50 @@ module SlashCommands
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
end
+ desc 'Set time estimate'
+ params '<1w 3d 2h 14m>'
+ condition do
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :estimate do |raw_duration|
+ time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration)
+
+ if time_estimate
+ @updates[:time_estimate] = time_estimate
+ end
+ end
+
+ desc 'Add or substract spent time'
+ params '<1h 30m | -1h 30m>'
+ condition do
+ current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
+ end
+ command :spend do |raw_duration|
+ time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration)
+
+ if time_spent
+ @updates[:spend_time] = { duration: time_spent, user: current_user }
+ end
+ end
+
+ desc 'Remove time estimate'
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :remove_estimate do
+ @updates[:time_estimate] = 0
+ end
+
+ desc 'Remove spent time'
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :remove_time_spent do
+ @updates[:spend_time] = { duration: :reset, user: current_user }
+ end
+
# This is a dummy command, so that it appears in the autocomplete commands
desc 'CC'
params '@user'
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 1ce66d50368..a11bca00687 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -21,7 +21,7 @@ module SystemNoteService
total_count = new_commits.length + existing_commits.length
commits_text = "#{total_count} commit".pluralize(total_count)
- body = "Added #{commits_text}:\n\n"
+ body = "added #{commits_text}\n\n"
body << existing_commit_summary(noteable, existing_commits, oldrev)
body << new_commit_summary(new_commits).join("\n")
body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})"
@@ -38,13 +38,13 @@ module SystemNoteService
#
# Example Note text:
#
- # "Assignee removed"
+ # "removed assignee"
#
- # "Reassigned to @rspeicher"
+ # "assigned to @rspeicher"
#
# Returns the created Note object
def change_assignee(noteable, project, author, assignee)
- body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}"
+ body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -59,11 +59,11 @@ module SystemNoteService
#
# Example Note text:
#
- # "Added ~1 and removed ~2 ~3 labels"
+ # "added ~1 and removed ~2 ~3 labels"
#
- # "Added ~4 label"
+ # "added ~4 label"
#
- # "Removed ~5 label"
+ # "removed ~5 label"
#
# Returns the created Note object
def change_label(noteable, project, author, added_labels, removed_labels)
@@ -85,7 +85,6 @@ module SystemNoteService
end
body << ' ' << 'label'.pluralize(labels_count)
- body = body.capitalize
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -99,14 +98,64 @@ module SystemNoteService
#
# Example Note text:
#
- # "Milestone removed"
+ # "removed milestone"
#
- # "Miletone changed to 7.11"
+ # "changed milestone to 7.11"
#
# Returns the created Note object
def change_milestone(noteable, project, author, milestone)
- body = 'Milestone '
- body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}"
+ body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project)}"
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
+ # Called when the estimated time of a Noteable is changed
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # time_estimate - Estimated time
+ #
+ # Example Note text:
+ #
+ # "Changed estimate of this issue to 3d 5h"
+ #
+ # Returns the created Note object
+
+ def change_time_estimate(noteable, project, author)
+ parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
+ body = if noteable.time_estimate == 0
+ "Removed time estimate on this #{noteable.human_class_name}"
+ else
+ "Changed time estimate of this #{noteable.human_class_name} to #{parsed_time}"
+ end
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
+ # Called when the spent time of a Noteable is changed
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # time_spent - Spent time
+ #
+ # Example Note text:
+ #
+ # "Added 2h 30m of time spent on this issue"
+ #
+ # Returns the created Note object
+
+ def change_time_spent(noteable, project, author)
+ time_spent = noteable.time_spent
+
+ if time_spent == :reset
+ body = "Removed time spent on this #{noteable.human_class_name}"
+ else
+ parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
+ action = time_spent > 0 ? 'Added' : 'Subtracted'
+ body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}"
+ end
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -121,50 +170,64 @@ module SystemNoteService
#
# Example Note text:
#
- # "Status changed to merged"
+ # "merged"
#
- # "Status changed to closed by bc17db76"
+ # "closed via bc17db76"
#
# Returns the created Note object
def change_status(noteable, project, author, status, source)
- body = "Status changed to #{status}"
- body << " by #{source.gfm_reference(project)}" if source
+ body = status.dup
+ body << " via #{source.gfm_reference(project)}" if source
create_note(noteable: noteable, project: project, author: author, note: body)
end
- # Called when 'merge when build succeeds' is executed
+ # Called when 'merge when pipeline succeeds' is executed
def merge_when_build_succeeds(noteable, project, author, last_commit)
- body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds"
+ body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds"
create_note(noteable: noteable, project: project, author: author, note: body)
end
- # Called when 'merge when build succeeds' is canceled
+ # Called when 'merge when pipeline succeeds' is canceled
def cancel_merge_when_build_succeeds(noteable, project, author)
- body = 'Canceled the automatic merge'
+ body = 'canceled the automatic merge'
create_note(noteable: noteable, project: project, author: author, note: body)
end
def remove_merge_request_wip(noteable, project, author)
- body = 'Unmarked this merge request as a Work In Progress'
+ body = 'unmarked as a **Work In Progress**'
create_note(noteable: noteable, project: project, author: author, note: body)
end
def add_merge_request_wip(noteable, project, author)
- body = 'Marked this merge request as a **Work In Progress**'
+ body = 'marked as a **Work In Progress**'
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
+ def add_merge_request_wip_from_commit(noteable, project, author, commit)
+ body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
def self.resolve_all_discussions(merge_request, project, author)
- body = "Resolved all discussions"
+ body = "resolved all discussions"
create_note(noteable: merge_request, project: project, author: author, note: body)
end
+ def discussion_continued_in_issue(discussion, project, author, issue)
+ body = "Added #{issue.to_reference} to continue this discussion"
+ note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
+ note_attributes[:type] = note_attributes.delete(:note_type)
+
+ create_note(note_attributes)
+ end
+
# Called when the title of a Noteable is changed
#
# noteable - Noteable object that responds to `title`
@@ -174,7 +237,7 @@ module SystemNoteService
#
# Example Note text:
#
- # "Title changed from **Old** to **New**"
+ # "changed title from **Old** to **New**"
#
# Returns the created Note object
def change_title(noteable, project, author, old_title)
@@ -185,7 +248,7 @@ module SystemNoteService
marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true)
marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true)
- body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**"
+ body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -197,11 +260,11 @@ module SystemNoteService
#
# Example Note text:
#
- # "Made the issue confidential"
+ # "made the issue confidential"
#
# Returns the created Note object
def change_issue_confidentiality(issue, project, author)
- body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible'
+ body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone'
create_note(noteable: issue, project: project, author: author, note: body)
end
@@ -216,11 +279,11 @@ module SystemNoteService
#
# Example Note text:
#
- # "Target branch changed from `Old` to `New`"
+ # "changed target branch from `Old` to `New`"
#
# Returns the created Note object
def change_branch(noteable, project, author, branch_type, old_branch, new_branch)
- body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize
+ body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -235,7 +298,7 @@ module SystemNoteService
#
# Example Note text:
#
- # "Restored target branch `feature`"
+ # "restored target branch `feature`"
#
# Returns the created Note object
def change_branch_presence(noteable, project, author, branch_type, branch, presence)
@@ -246,18 +309,18 @@ module SystemNoteService
'deleted'
end
- body = "#{verb} #{branch_type} branch `#{branch}`".capitalize
+ body = "#{verb} #{branch_type} branch `#{branch}`"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when a branch is created from the 'new branch' button on a issue
# Example note text:
#
- # "Started branch `201-issue-branch-button`"
+ # "created branch `201-issue-branch-button`"
def new_issue_branch(issue, project, author, branch)
link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
- body = "Started branch [`#{branch}`](#{link})"
+ body = "created branch [`#{branch}`](#{link})"
create_note(noteable: issue, project: project, author: author, note: body)
end
@@ -269,11 +332,11 @@ module SystemNoteService
#
# Example Note text:
#
- # "Mentioned in #1"
+ # "mentioned in #1"
#
- # "Mentioned in !2"
+ # "mentioned in !2"
#
- # "Mentioned in 54f7727c"
+ # "mentioned in 54f7727c"
#
# See cross_reference_note_content.
#
@@ -303,12 +366,12 @@ module SystemNoteService
end
def cross_reference?(note_text)
- note_text.start_with?(cross_reference_note_prefix)
+ note_text =~ /\A#{cross_reference_note_prefix}/i
end
# Check if a cross-reference is disallowed
#
- # This method prevents adding a "Mentioned in !1" note on every single commit
+ # This method prevents adding a "mentioned in !1" note on every single commit
# in a merge request. Additionally, it prevents the creation of references to
# external issues (which would fail).
#
@@ -370,12 +433,12 @@ module SystemNoteService
#
# Example Note text:
#
- # "Soandso marked the task Whatever as completed."
+ # "marked the task Whatever as completed."
#
# Returns the created Note object
def change_task_status(noteable, project, author, new_task)
status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
- body = "Marked the task **#{new_task.source}** as #{status_label}"
+ body = "marked the task **#{new_task.source}** as #{status_label}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -388,7 +451,7 @@ module SystemNoteService
#
# Example Note text:
#
- # "Moved to some_namespace/project_new#11"
+ # "moved to some_namespace/project_new#11"
#
# Returns the created Note object
def noteable_moved(noteable, project, noteable_ref, author, direction:)
@@ -397,7 +460,7 @@ module SystemNoteService
end
cross_reference = noteable_ref.to_reference(project)
- body = "Moved #{direction} #{cross_reference}"
+ body = "moved #{direction} #{cross_reference}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -405,10 +468,12 @@ module SystemNoteService
def notes_for_mentioner(mentioner, noteable, notes)
if mentioner.is_a?(Commit)
- notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}")
+ text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
+ notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize)
else
gfm_reference = mentioner.gfm_reference(noteable.project)
- notes.where(note: cross_reference_note_content(gfm_reference))
+ text = cross_reference_note_content(gfm_reference)
+ notes.where(note: [text, text.capitalize])
end
end
@@ -417,7 +482,7 @@ module SystemNoteService
end
def cross_reference_note_prefix
- 'Mentioned in '
+ 'mentioned in '
end
def cross_reference_note_content(gfm_reference)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index f8e6b2ef094..1bd6ce416ab 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -98,10 +98,12 @@ class TodoService
# When a build fails on the HEAD of a merge request we should:
#
- # * create a todo for that user to fix it
+ # * create a todo for author of MR to fix it
+ # * create a todo for merge_user to keep an eye on it
#
def merge_request_build_failed(merge_request)
- create_build_failed_todo(merge_request)
+ create_build_failed_todo(merge_request, merge_request.author)
+ create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
end
# When a new commit is pushed to a merge request we should:
@@ -115,11 +117,21 @@ class TodoService
# When a build is retried to a merge request we should:
#
# * mark all pending todos related to the merge request for the author as done
+ # * mark all pending todos related to the merge request for the merge_user as done
#
def merge_request_build_retried(merge_request)
mark_pending_todos_as_done(merge_request, merge_request.author)
+ mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
end
-
+
+ # When a merge request could not be automatically merged due to its unmergeable state we should:
+ #
+ # * create a todo for a merge_user
+ #
+ def merge_request_became_unmergeable(merge_request)
+ create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
+ end
+
# When create a note we should:
#
# * mark all pending todos related to the noteable for the note author as done
@@ -236,10 +248,14 @@ class TodoService
create_todos(mentioned_users, attributes)
end
- def create_build_failed_todo(merge_request)
- author = merge_request.author
- attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED)
- create_todos(author, attributes)
+ def create_build_failed_todo(merge_request, todo_author)
+ attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::BUILD_FAILED)
+ create_todos(todo_author, attributes)
+ end
+
+ def create_unmergeable_todo(merge_request, merge_user)
+ attributes = attributes_for_todo(merge_request.project, merge_request, merge_user, Todo::UNMERGEABLE)
+ create_todos(merge_user, attributes)
end
def attributes_for_target(target)
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
index 0ee1ff2d7d9..b7c36651968 100644
--- a/app/services/update_release_service.rb
+++ b/app/services/update_release_service.rb
@@ -1,5 +1,3 @@
-require_relative 'base_service'
-
class UpdateReleaseService < BaseService
def execute(tag_name, release_description)
repository = project.repository
diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb
new file mode 100644
index 00000000000..2469b4f0d7c
--- /dev/null
+++ b/app/services/user_project_access_changed_service.rb
@@ -0,0 +1,9 @@
+class UserProjectAccessChangedService
+ def initialize(user_ids)
+ @user_ids = Array.wrap(user_ids)
+ end
+
+ def execute
+ AuthorizedProjectsWorker.bulk_perform_async(@user_ids.map { |id| [id] })
+ end
+end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
new file mode 100644
index 00000000000..2d211d5ebbe
--- /dev/null
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -0,0 +1,127 @@
+module Users
+ # Service for refreshing the authorized projects of a user.
+ #
+ # This particular service class can not be used to update data for the same
+ # user concurrently. Doing so could lead to an incorrect state. To ensure this
+ # doesn't happen a caller must synchronize access (e.g. using
+ # `Gitlab::ExclusiveLease`).
+ #
+ # Usage:
+ #
+ # user = User.find_by(username: 'alice')
+ # service = Users::RefreshAuthorizedProjectsService.new(some_user)
+ # service.execute
+ class RefreshAuthorizedProjectsService
+ attr_reader :user
+
+ LEASE_TIMEOUT = 1.minute.to_i
+
+ # user - The User for which to refresh the authorized projects.
+ def initialize(user)
+ @user = user
+
+ # We need an up to date User object that has access to all relations that
+ # may have been created earlier. The only way to ensure this is to reload
+ # the User object.
+ user.reload
+ end
+
+ def execute
+ lease_key = "refresh_authorized_projects:#{user.id}"
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. If we don't do so we may end up
+ # not updating the list of authorized projects properly. To prevent
+ # hammering Redis too much we'll wait for a bit between retries.
+ sleep(1)
+ end
+
+ begin
+ execute_without_lease
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
+ end
+
+ # This method returns the updated User object.
+ def execute_without_lease
+ current = current_authorizations_per_project
+ fresh = fresh_access_levels_per_project
+
+ remove = current.each_with_object([]) do |(project_id, row), array|
+ # rows not in the new list or with a different access level should be
+ # removed.
+ if !fresh[project_id] || fresh[project_id] != row.access_level
+ array << row.project_id
+ end
+ end
+
+ add = fresh.each_with_object([]) do |(project_id, level), array|
+ # rows not in the old list or with a different access level should be
+ # added.
+ if !current[project_id] || current[project_id].access_level != level
+ array << [user.id, project_id, level]
+ end
+ end
+
+ update_authorizations(remove, add)
+ end
+
+ # Updates the list of authorizations for the current user.
+ #
+ # remove - The IDs of the authorization rows to remove.
+ # add - Rows to insert in the form `[user id, project id, access level]`
+ def update_authorizations(remove = [], add = [])
+ return if remove.empty? && add.empty? && user.authorized_projects_populated
+
+ User.transaction do
+ user.remove_project_authorizations(remove) unless remove.empty?
+ ProjectAuthorization.insert_authorizations(add) unless add.empty?
+ user.set_authorized_projects_column
+ end
+
+ # Since we batch insert authorization rows, Rails' associations may get
+ # out of sync. As such we force a reload of the User object.
+ user.reload
+ end
+
+ def fresh_access_levels_per_project
+ fresh_authorizations.each_with_object({}) do |row, hash|
+ hash[row.project_id] = row.access_level
+ end
+ end
+
+ def current_authorizations_per_project
+ current_authorizations.each_with_object({}) do |row, hash|
+ hash[row.project_id] = row
+ end
+ end
+
+ def current_authorizations
+ user.project_authorizations.select(:project_id, :access_level)
+ end
+
+ def fresh_authorizations
+ ProjectAuthorization.
+ unscoped.
+ select('project_id, MAX(access_level) AS access_level').
+ from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}").
+ group(:project_id)
+ end
+
+ private
+
+ # Returns a union query of projects that the user is authorized to access
+ def project_authorizations_union
+ relations = [
+ user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
+ user.groups_projects.select_for_project_authorization,
+ user.projects.select_for_project_authorization,
+ user.groups.joins(:shared_projects).select_for_project_authorization
+ ]
+
+ Gitlab::SQL::Union.new(relations)
+ end
+ end
+end
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index b6c52ddac7a..86f317dcd18 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -1,4 +1,4 @@
-class ArtifactUploader < CarrierWave::Uploader::Base
+class ArtifactUploader < GitlabUploader
storage :file
attr_accessor :build, :field
@@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base
def exists?
file.try(:exists?)
end
-
- def move_to_cache
- true
- end
-
- def move_to_store
- true
- end
end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index fb3b5dfecd0..cfcb877cc3e 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,4 +1,4 @@
-class AttachmentUploader < CarrierWave::Uploader::Base
+class AttachmentUploader < GitlabUploader
include UploaderHelper
storage :file
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 71ff14a3f20..265cea2d2c6 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,19 +1,24 @@
-class AvatarUploader < CarrierWave::Uploader::Base
+class AvatarUploader < GitlabUploader
include UploaderHelper
storage :file
- after :store, :reset_events_cache
-
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
- def reset_events_cache(file)
- model.reset_events_cache if model.is_a?(User)
- end
-
def exists?
model.avatar.file && model.avatar.file.exists?
end
+
+ # We set move_to_store and move_to_cache to 'false' to prevent stealing
+ # the avatar file from a project when forking it.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
+ def move_to_store
+ false
+ end
+
+ def move_to_cache
+ false
+ end
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 3ac6030c21c..47bef7cd1e4 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,4 +1,4 @@
-class FileUploader < CarrierWave::Uploader::Base
+class FileUploader < GitlabUploader
include UploaderHelper
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
new file mode 100644
index 00000000000..02d7c601d6c
--- /dev/null
+++ b/app/uploaders/gitlab_uploader.rb
@@ -0,0 +1,11 @@
+class GitlabUploader < CarrierWave::Uploader::Base
+ # Reduce disk IO
+ def move_to_cache
+ true
+ end
+
+ # Reduce disk IO
+ def move_to_store
+ true
+ end
+end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index 4f356dd663e..faab539b8e0 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -1,4 +1,4 @@
-class LfsObjectUploader < CarrierWave::Uploader::Base
+class LfsObjectUploader < GitlabUploader
storage :file
def store_dir
@@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base
"#{Gitlab.config.lfs.storage_path}/tmp/cache"
end
- def move_to_cache
- true
- end
-
- def move_to_store
- true
- end
-
def exists?
file.try(:exists?)
end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index b10ad71d052..fbaea2744a3 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,6 +1,6 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff]
+ IMAGE_EXT = %w[png jpg jpeg gif bmp tiff svg]
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 2821ecf0a88..eb3ed31b65b 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -35,8 +35,22 @@ class NamespaceValidator < ActiveModel::EachValidator
users
].freeze
+ def self.valid?(value)
+ !reserved?(value) && follow_format?(value)
+ end
+
+ def self.reserved?(value)
+ RESERVED.include?(value)
+ end
+
+ def self.follow_format?(value)
+ value =~ Gitlab::Regex.namespace_regex
+ end
+
+ delegate :reserved?, :follow_format?, to: :class
+
def validate_each(record, attribute, value)
- unless value =~ Gitlab::Regex.namespace_regex
+ unless follow_format?(value)
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
@@ -44,10 +58,4 @@ class NamespaceValidator < ActiveModel::EachValidator
record.errors.add(attribute, "#{value} is a reserved name")
end
end
-
- private
-
- def reserved?(value)
- RESERVED.include?(value)
- end
end
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
new file mode 100644
index 00000000000..36279daa743
--- /dev/null
+++ b/app/validators/project_path_validator.rb
@@ -0,0 +1,37 @@
+# ProjectPathValidator
+#
+# Custom validator for GitLab project path values.
+#
+# Values are checked for formatting and exclusion from a list of reserved path
+# names.
+class ProjectPathValidator < ActiveModel::EachValidator
+ # All project routes with wildcard argument must be listed here.
+ # Otherwise it can lead to routing issues when route considered as project name.
+ #
+ # Example:
+ # /group/project/tree/deploy_keys
+ #
+ # without tree as reserved name routing can match 'group/project' as group name,
+ # 'tree' as project name and 'deploy_keys' as route.
+ #
+ RESERVED = (NamespaceValidator::RESERVED -
+ %w[dashboard help ci admin search notes services assets profile public] +
+ %w[tree commits wikis new edit create update logs_tree
+ preview blob blame raw files create_dir find_file]).freeze
+
+ def self.valid?(value)
+ !reserved?(value)
+ end
+
+ def self.reserved?(value)
+ RESERVED.include?(value)
+ end
+
+ delegate :reserved?, to: :class
+
+ def validate_each(record, attribute, value)
+ if reserved?(value)
+ record.errors.add(attribute, "#{value} is a reserved name")
+ end
+ end
+end
diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml
index 2741eb44357..d50b4071745 100644
--- a/app/views/abuse_report_mailer/notify.html.haml
+++ b/app/views/abuse_report_mailer/notify.html.haml
@@ -1,7 +1,7 @@
%p
- #{link_to @abuse_report.user.name, user_url(@abuse_report.user)}
- (@#{@abuse_report.user.username}) was reported for abuse by
- #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)}
+ #{link_to @abuse_report.user.name, user_url(@abuse_report.user)}
+ (@#{@abuse_report.user.username}) was reported for abuse by
+ #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)}
(@#{@abuse_report.reporter.username}).
%blockquote
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 7bbc75db9ff..c4b748d0ab8 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -4,7 +4,7 @@
.abuse-reports
- if @abuse_reports.present?
.table-holder
- %table.table
+ %table.table.responsive-table
%thead.hidden-sm.hidden-xs
%tr
%th User
@@ -13,8 +13,6 @@
%th Action
= render @abuse_reports
- else
- .no-reports
- %span.pull-left
- There are no abuse reports!
- .pull-left
- = emoji_icon 'tada'
+ .empty-state
+ .text-center
+ %h4 There are no abuse reports! #{emoji_icon 'tada'}
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 450ec322f2c..558bbe07b16 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -22,9 +22,8 @@
.form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10
- - data_attrs = { toggle: 'buttons' }
- .btn-group{ data: data_attrs }
- - restricted_level_checkboxes('restricted-visibility-help').each do |level|
+ - restricted_level_checkboxes('restricted-visibility-help').each do |level|
+ .checkbox
= level
%span.help-block#restricted-visibility-help
Selected levels cannot be used by non-admin users for projects or snippets.
@@ -32,10 +31,8 @@
.form-group
= f.label :import_sources, class: 'control-label col-sm-2'
.col-sm-10
- - data_attrs = { toggle: 'buttons' }
- .btn-group{ data: data_attrs }
- - import_sources_checkboxes('import-sources-help').each do |source|
- = source
+ - import_sources_checkboxes('import-sources-help').each do |source|
+ .checkbox= source
%span.help-block#import-sources-help
Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
= link_to "(?)", help_page_path("integration/github")
@@ -284,6 +281,31 @@
results in fewer but larger UDP packets being sent.
%fieldset
+ %legend Background Jobs
+ %p
+ These settings require a restart to take effect.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :sidekiq_throttling_enabled do
+ = f.check_box :sidekiq_throttling_enabled
+ Enable Sidekiq Job Throttling
+ .help-block
+ Limit the amount of resources slow running jobs are assigned.
+ .form-group
+ = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
+ .help-block
+ Choose which queues you wish to throttle.
+ .form-group
+ = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
+ .help-block
+ The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
+
+ %fieldset
%legend Spam and Anti-bot Protection
.form-group
.col-sm-offset-2.col-sm-10
@@ -299,7 +321,7 @@
= f.text_field :recaptcha_site_key, class: 'form-control'
.help-block
Generate site and private keys at
- %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha
+ %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
.form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
@@ -320,7 +342,7 @@
= f.text_field :akismet_api_key, class: 'form-control'
.help-block
Generate API key at
- %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com
+ %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
%fieldset
%legend Abuse reports
@@ -399,6 +421,23 @@
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
%fieldset
+ %legend PlantUML
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :plantuml_enabled do
+ = f.check_box :plantuml_enabled
+ Enable PlantUML
+ .form-group
+ = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
+ .help-block
+ Allow rendering of
+ = link_to "PlantUML", "http://plantuml.com"
+ diagrams in Asciidoc documents using an external PlantUML service.
+
+ %fieldset
%legend Usage statistics
.form-group
.col-sm-offset-2.col-sm-10
@@ -421,7 +460,16 @@
Some email servers do not support overriding the email sender name.
Enable this option to include the name of the author of the issue,
merge request or comment in the email body instead.
-
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :html_emails_enabled do
+ = f.check_box :html_emails_enabled
+ Enable HTML emails
+ .help-block
+ By default GitLab sends emails in HTML and plain text formats so mail
+ clients can choose what format to use. Disable this option if you only
+ want to send emails in plain text format.
%fieldset
%legend Automatic Git repository housekeeping
.form-group
diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml
index 042971e1eed..82781f6716d 100644
--- a/app/views/admin/applications/_delete_form.html.haml
+++ b/app/views/admin/applications/_delete_form.html.haml
@@ -1,4 +1,4 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag admin_application_path(application) do
- %input{:name => "_method", :type => "hidden", :value => "delete"}/
+ %input{ :name => "_method", :type => "hidden", :value => "delete" }/
= submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 4aacbb8cd77..c689b26d6e6 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -18,6 +18,12 @@
Use
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
+
+ .form-group
+ = f.label :scopes, class: 'col-sm-2 control-label'
+ .col-sm-10
+ = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
+
.form-actions
= f.submit 'Submit', class: "btn btn-save wide"
= link_to "Cancel", admin_applications_path, class: "btn btn-default"
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index f8cd98f0ec4..b3a3b4c1d45 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -15,7 +15,7 @@
%th
%tbody.oauth-applications
- @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
+ %tr{ :id => "application_#{application.id}" }
%td= link_to application.name, admin_application_path(application)
%td= application.redirect_uri
%td= application.access_tokens.map(&:resource_owner_id).uniq.count
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 3eb9d61972b..14683cc66e9 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -2,8 +2,7 @@
%h3.page-title
Application: #{@application.name}
-
-.table-holder
+.table-holder.oauth-application-show
%table.table
%tr
%td
@@ -23,6 +22,9 @@
- @application.redirect_uri.split.each do |uri|
%div
%span.monospace= uri
+
+ = render "shared/tokens/scopes_list", token: @application
+
.form-actions
= link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 05855db963a..4f982a6e369 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -43,4 +43,4 @@
.panel.panel-default
- %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"}
+ %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: none" }
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index c05538a393c..4f2ae081d7a 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -10,7 +10,7 @@
%br.clearfix
--if @broadcast_messages.any?
+- if @broadcast_messages.any?
%table.table
%thead
%tr
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 26a8846b609..5e3f105d41f 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -14,5 +14,5 @@
.row-content-block.second-block
#{(@scope || 'all').capitalize} builds
- %ul.content-list.builds-content-list
+ %ul.content-list.builds-content-list.admin-builds-table
= render "projects/builds/table", builds: @builds, admin: true
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index ec40391a3e3..b5f96363230 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -8,7 +8,7 @@
%span
Overview
= nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
+ = link_to admin_projects_path, title: 'Projects' do
%span
Projects
= nav_link(controller: :users) do
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 1db2150f336..5238623e936 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -113,15 +113,15 @@
%hr
.row
.col-sm-4
- .light-well
+ .light-well.well-centered
%h4 Projects
.data
- = link_to admin_namespaces_projects_path do
+ = link_to admin_projects_path do
%h1= number_with_delimiter(Project.cached_count)
%hr
= link_to('New Project', new_project_path, class: "btn btn-new")
.col-sm-4
- .light-well
+ .light-well.well-centered
%h4 Users
.data
= link_to admin_users_path do
@@ -129,7 +129,7 @@
%hr
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
.col-sm-4
- .light-well
+ .light-well.well-centered
%h4 Groups
.data
= link_to admin_groups_path do
@@ -143,7 +143,7 @@
%hr
- @projects.each do |project|
%p
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated'
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
%span.light.pull-right
#{time_ago_with_tooltip(project.created_at)}
@@ -152,7 +152,7 @@
%hr
- @users.each do |user|
%p
- = link_to [:admin, user], class: 'str-truncated' do
+ = link_to [:admin, user], class: 'str-truncated-60' do
= user.name
%span.light.pull-right
#{time_ago_with_tooltip(user.created_at)}
@@ -162,7 +162,7 @@
%hr
- @groups.each do |group|
%p
- = link_to [:admin, group], class: 'str-truncated' do
+ = link_to [:admin, group], class: 'str-truncated-60' do
= group.name
%span.light.pull-right
#{time_ago_with_tooltip(group.created_at)}
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 149593e7f46..7b71bb5b287 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -1,27 +1,34 @@
- page_title "Deploy Keys"
-.panel.panel-default.prepend-top-default
- .panel-heading
- Public deploy keys (#{@deploy_keys.count})
- .controls
- = link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm"
- - if @deploy_keys.any?
- .table-holder
- %table.table
- %thead.panel-heading
+
+%h3.page-title.deploy-keys-title
+ Public deploy keys (#{@deploy_keys.count})
+ .pull-right
+ = link_to 'New Deploy Key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted'
+
+- if @deploy_keys.any?
+ .table-holder.deploy-keys-list
+ %table.table
+ %thead
+ %tr
+ %th.col-sm-2 Title
+ %th.col-sm-4 Fingerprint
+ %th.col-sm-2 Write access allowed
+ %th.col-sm-2 Added at
+ %th.col-sm-2
+ %tbody
+ - @deploy_keys.each do |deploy_key|
%tr
- %th Title
- %th Fingerprint
- %th Added at
- %th
- %tbody
- - @deploy_keys.each do |deploy_key|
- %tr
- %td
- %strong= deploy_key.title
- %td
- %code.key-fingerprint= deploy_key.fingerprint
- %td
- %span.cgray
- added #{time_ago_with_tooltip(deploy_key.created_at)}
- %td
- = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right"
+ %td
+ %strong= deploy_key.title
+ %td
+ %code.key-fingerprint= deploy_key.fingerprint
+ %td
+ - if deploy_key.can_push?
+ Yes
+ - else
+ No
+ %td
+ %span.cgray
+ added #{time_ago_with_tooltip(deploy_key.created_at)}
+ %td
+ = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key pull-right'
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
index 5c410a695bf..a064efc231f 100644
--- a/app/views/admin/deploy_keys/new.html.haml
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -16,6 +16,14 @@
Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh/README")
= f.text_area :key, class: "form-control thin_area", rows: 5
+ .form-group
+ .control-label
+ .col-sm-10
+ = f.label :can_push do
+ = f.check_box :can_push
+ %strong Write access allowed
+ %p.light.append-bottom-0
+ Allow this key to push to repository as well? (Default only allows pull access.)
.form-actions
= f.submit 'Create', class: "btn-create btn"
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 817910f7ddf..589f4557b52 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -7,7 +7,7 @@
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
- = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+ = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 664bb417c6a..e3a77dfdf10 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -2,9 +2,12 @@
%li.group-row{ class: css_class }
.controls
- = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
+ = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
.stats
+ %span.badge
+ = storage_counter(group.storage_size)
+
%span
= icon('bookmark')
= number_with_delimiter(group.projects.count)
@@ -13,14 +16,14 @@
= icon('users')
= number_with_delimiter(group.users.count)
- %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) }
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to [:admin, group], class: 'group-name' do
- = group.name
+ = group.full_name
- if group.description.present?
.description
diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml
index eb09a6328ed..c2b9807015d 100644
--- a/app/views/admin/groups/edit.html.haml
+++ b/app/views/admin/groups/edit.html.haml
@@ -1,4 +1,4 @@
- page_title "Edit", @group.name, "Groups"
%h3.page-title Edit group: #{@group.name}
%hr
-= render 'form'
+= render 'form', visibility_level: @group.visibility_level
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 794f910a61f..07775247cfd 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -27,6 +27,8 @@
= sort_title_recently_updated
= link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do
= sort_title_oldest_updated
+ = link_to admin_groups_path(sort: sort_value_largest_group, name: project_name) do
+ = sort_title_largest_group
= link_to new_admin_group_path, class: "btn btn-new" do
New Group
%ul.content-list
diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml
index c81ee552ac3..8f9fe96249f 100644
--- a/app/views/admin/groups/new.html.haml
+++ b/app/views/admin/groups/new.html.haml
@@ -1,4 +1,4 @@
- page_title "New Group"
%h3.page-title New group
%hr
-= render 'form'
+= render 'form', visibility_level: default_group_visibility
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 40871e32913..30b3fabdd7e 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -1,8 +1,8 @@
- page_title @group.name, "Groups"
%h3.page-title
- Group: #{@group.name}
+ Group: #{@group.full_name}
- = link_to edit_admin_group_path(@group), class: "btn pull-right" do
+ = link_to admin_group_edit_path(@group), class: "btn pull-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
@@ -39,6 +39,18 @@
= @group.created_at.to_s(:medium)
%li
+ %span.light Storage:
+ %strong= storage_counter(@group.storage_size)
+ (
+ = storage_counter(@group.repository_size)
+ repositories,
+ = storage_counter(@group.build_artifacts_size)
+ build artifacts,
+ = storage_counter(@group.lfs_objects_size)
+ LFS
+ )
+
+ %li
%span.light Group Git LFS status:
%strong
= group_lfs_status(@group)
@@ -55,8 +67,8 @@
%li
%strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
- %span.label.label-gray
- = repository_size(project)
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
%span.pull-right.light
%span.monospace= project.path_with_namespace + ".git"
.panel-footer
@@ -73,8 +85,8 @@
%li
%strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
- %span.label.label-gray
- = repository_size(project)
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
%span.pull-right.light
%span.monospace= project.path_with_namespace + ".git"
@@ -88,10 +100,10 @@
Read more about project permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
- = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
+ = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
= users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
- %div.prepend-top-10
+ .prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
= button_tag 'Add users to group', class: "btn btn-create"
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index c217490963f..551edf14361 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -29,7 +29,7 @@
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
- %div.prepend-top-default
+ .prepend-top-default
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
= f.label :push_events, class: 'list-label' do
@@ -54,7 +54,7 @@
= f.submit "Add System Hook", class: "btn btn-create"
%hr
--if @hooks.any?
+- if @hooks.any?
.panel.panel-default
.panel-heading
System hooks (#{@hooks.count})
@@ -70,4 +70,3 @@
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
-
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index be224d66855..77b174fbb27 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,4 +1,4 @@
-%li{id: dom_id(label)}
+%li{ id: dom_id(label) }
.label-row
= render_colored_label(label, tooltip: false)
= markdown_field(label, :description)
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 824edd171f3..0a954c20fcd 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -8,7 +8,7 @@
%div{ class: container_class }
%ul.nav-links.log-tabs
- loggers.each do |klass|
- %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
+ %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }>
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
.row-content-block
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index b37b8d4fee7..2e6f03fcde0 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -7,7 +7,7 @@
%div{ class: container_class }
.top-area
.prepend-top-default
- = form_tag admin_namespaces_projects_path, method: :get do |f|
+ = form_tag admin_projects_path, method: :get do |f|
.search-holder
.search-field-holder
= search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name'
@@ -41,19 +41,19 @@
= button_tag "Search", class: "btn btn-primary btn-search"
%ul.nav-links
- - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path }
+ - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
= nav_link(opts) do
- = link_to admin_namespaces_projects_path do
+ = link_to admin_projects_path do
All
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
Private
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
Internal
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
.nav-controls
@@ -69,8 +69,8 @@
.controls
- if project.archived
%span.label.label-warning archived
- %span.label.label-gray
- = repository_size(project)
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
.title
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 6c7c3c48604..2967da6e692 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -65,9 +65,16 @@
= @project.repository.path_to_repo
%li
- %span.light Size
- %strong
- = repository_size(@project)
+ %span.light Storage:
+ %strong= storage_counter(@project.statistics.storage_size)
+ (
+ = storage_counter(@project.statistics.repository_size)
+ repository,
+ = storage_counter(@project.statistics.build_artifacts_size)
+ build artifacts,
+ = storage_counter(@project.statistics.lfs_objects_size)
+ LFS
+ )
%li
%span.light last commit:
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 64893b38c58..975bd950ae1 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -1,4 +1,4 @@
-%tr{id: dom_id(runner)}
+%tr{ id: dom_id(runner) }
%td
- if runner.shared?
%span.label.label-success shared
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 73038164056..ca503e35623 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -91,7 +91,7 @@
%strong ##{build.id}
%td.status
- = ci_status_with_icon(build.status)
+ = render 'ci/status/badge', status: build.detailed_status(current_user)
%td.status
- if project
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index cdbfc60f9a4..e5b8ebdf613 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -4,7 +4,8 @@
%p #{@service.description} template
= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |form|
- = render 'shared/service_settings', form: form
+ = render 'shared/service_settings', form: form, subject: @service
- .form-actions
- = form.submit 'Save', class: 'btn btn-save'
+ .footer-block.row-content-block
+ .form-actions
+ = form.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index ce5e21e54cc..9984e733956 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -15,10 +15,8 @@
%ul.nav-links
= nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user)
- = nav_link(path: 'users#groups') do
- = link_to "Groups", groups_admin_user_path(@user)
= nav_link(path: 'users#projects') do
- = link_to "Projects", projects_admin_user_path(@user)
+ = link_to "Groups and projects", projects_admin_user_path(@user)
= nav_link(path: 'users#keys') do
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 4bf1c9cde3c..3b5c713ac2d 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -1,8 +1,8 @@
-%li.user-row
+%li.flex-row
.user-avatar
= image_tag avatar_icon(user), class: "avatar", alt: ''
- .user-details
- .user-name
+ .row-main-content
+ .user-name.row-title.str-truncated-100
= link_to user.name, [:admin, user]
- if user.blocked?
%span.label.label-danger blocked
@@ -12,13 +12,13 @@
%span.label.label-default External
- if user == current_user
%span It's you!
- .user-email
+ .row-second-line.str-truncated-100
= mail_to user.email, user.email
.controls
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn'
- unless user == current_user
.dropdown.inline
- %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } }
+ %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', data: { toggle: 'dropdown' } }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/admin/users/groups.html.haml b/app/views/admin/users/groups.html.haml
deleted file mode 100644
index 8f6d13b881a..00000000000
--- a/app/views/admin/users/groups.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-- page_title "Groups", @user.name, "Users"
-= render 'admin/users/head'
-
-- group_members = @user.group_members.includes(:source)
-- if group_members.any?
- .panel.panel-default
- .panel-heading Groups:
- %ul.well-list
- - group_members.each do |group_member|
- - group = group_member.group
- %li.group_member
- %span{class: ("list-item-name" unless group_member.owner?)}
- %strong= link_to group.name, admin_group_path(group)
- .pull-right
- %span.light= group_member.human_access
- - unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- %i.fa.fa-times.fa-inverse
-- else
- .nothing-here-block This user has no groups.
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index d3038ae644f..4dc44225d49 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -68,7 +68,7 @@
%small.badge= number_with_delimiter(User.without_projects.count)
.fade-right
- %ul.users-list.content-list
+ %ul.flex-list.content-list
- if @users.empty?
%li
.nothing-here-block No users found.
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 84b9ceb23b3..15eaf1c0e67 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -1,15 +1,21 @@
-- page_title "Projects", @user.name, "Users"
+- page_title "Groups and projects", @user.name, "Users"
= render 'admin/users/head'
- if @user.groups.any?
.panel.panel-default
.panel-heading Group projects
%ul.well-list
- - @user.groups.each do |group|
- %li
- %strong= group.name
+ - @user.group_members.includes(:source).each do |group_member|
+ - group = group_member.group
+ %li.group_member
+ %strong= link_to group.name, admin_group_path(group)
&ndash; access to
#{pluralize(group.projects.count, 'project')}
+ .pull-right
+ %span.light.vertical-align-middle= group_member.human_access
+ - unless group_member.owner?
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from group' do
+ %i.fa.fa-times.fa-inverse
.row
.col-md-6
@@ -35,8 +41,8 @@
- if member.owner?
%span.light Owner
- else
- %span.light= member.human_access
+ %span.light.vertical-align-middle= member.human_access
- if member.respond_to? :project
- = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
+ = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from project' do
%i.fa.fa-times
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 76c9ed0ee8b..a71240986c9 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -40,7 +40,7 @@
%li.two-factor-status
%span.light Two-factor Authentication:
- %strong{class: @user.two_factor_enabled? ? 'cgreen' : 'cred'}
+ %strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' }
- if @user.two_factor_enabled?
Enabled
= link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-xs btn-remove pull-right', title: 'Disable Two-factor Authentication'
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index fbe3ab912b6..e3305e21e96 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,7 +1,9 @@
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
- %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
+ %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
+ class: (award_state_class(awards, current_user)),
+ data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false)
%span.award-control-text.js-counter
= awards.count
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index 61c7cce20b2..c91602fcff7 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -36,7 +36,7 @@
- if build[:allow_failure]
%b Allowed to fail
--else
+- else
%p
%b Status:
syntax is incorrect
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index 889086c62b1..95eb9a57152 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,20 +1,25 @@
- page_title "CI Lint"
- page_description "Validate your GitLab CI configuration file"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
%h2 Check your .gitlab-ci.yml
-%hr
-.row
- = form_tag ci_lint_path, method: :post do
- .form-group
- = label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap')
+.ci-linter
+ .row
+ = form_tag ci_lint_path, method: :post do
+ .form-group
+ .col-sm-12
+ .file-holder
+ .file-title.clearfix
+ Content of .gitlab-ci.yml
+ #ci-editor.ci-editor #{@content}
+ = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
- = text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true)
- .col-sm-12
- .pull-left.prepend-top-10
- = submit_tag('Validate', class: 'btn btn-success submit-yml')
+ .pull-left.prepend-top-10
+ = submit_tag('Validate', class: 'btn btn-success submit-yml')
-.row.prepend-top-20
- .col-sm-12
- .results
- = render partial: 'create' if defined?(@status)
+ .row.prepend-top-20
+ .col-sm-12
+ .results.ci-template
+ = render partial: 'create' if defined?(@status)
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
new file mode 100644
index 00000000000..601fb7f0f3f
--- /dev/null
+++ b/app/views/ci/status/_badge.html.haml
@@ -0,0 +1,11 @@
+- status = local_assigns.fetch(:status)
+- css_classes = "ci-status ci-#{status.group}"
+
+- if status.has_details?
+ = link_to status.details_path, class: css_classes do
+ = custom_icon(status.icon)
+ = status.text
+- else
+ %span{ class: css_classes }
+ = custom_icon(status.icon)
+ = status.text
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
new file mode 100644
index 00000000000..8dea3479f82
--- /dev/null
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -0,0 +1,19 @@
+-# Renders the content of each li in the dropdown
+
+- subject = local_assigns.fetch(:subject)
+- status = subject.detailed_status(current_user)
+- klass = "ci-status-icon ci-status-icon-#{status.group}"
+- tooltip = "#{subject.name} - #{status.label}"
+
+- if status.has_details?
+ = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip } do
+ %span{ class: klass }= custom_icon(status.icon)
+ %span.ci-build-text= subject.name
+- else
+ .mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip } }
+ %span{ class: klass }= custom_icon(status.icon)
+ %span.ci-build-text= subject.name
+
+- if status.has_action?
+ = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
+ = icon(status.action_icon, class: status.action_class)
diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml
new file mode 100644
index 00000000000..dd2f649de9a
--- /dev/null
+++ b/app/views/ci/status/_graph_badge.html.haml
@@ -0,0 +1,20 @@
+-# Renders the graph node with both the status icon, status name and action icon
+
+- subject = local_assigns.fetch(:subject)
+- status = subject.detailed_status(current_user)
+- klass = "ci-status-icon ci-status-icon-#{status.group}"
+- tooltip = "#{subject.name} - #{status.label}"
+
+- if status.has_details?
+ = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip } do
+ %span{ class: klass }= custom_icon(status.icon)
+ .ci-status-text= subject.name
+- else
+ .build-content.has-tooltip{ data: { toggle: 'tooltip', title: tooltip } }
+ %span{ class: klass }= custom_icon(status.icon)
+ .ci-status-text= subject.name
+
+- if status.has_action?
+ = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
+ %i.ci-action-icon-wrapper
+ = icon(status.action_icon, class: status.action_class)
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index b78e70ebc1e..02b94beee92 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,7 +1,7 @@
%ul.nav-links
- %li{ class: ("active" unless params[:filter]) }
+ %li{ class: ("active" unless params[:filter]) }>
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
- %li{ class: ("active" if params[:filter] == 'starred') }
+ %li{ class: ("active" if params[:filter] == 'starred') }>
= link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
Starred Projects
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index f7abad54286..48b0fd504f4 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -4,13 +4,13 @@
%ul.nav-links
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
- Your Projects
+ Your projects
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
- Starred Projects
+ Starred projects
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
- Explore Projects
+ Explore projects
.nav-controls
= form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index b25e8ea1f0c..02e90bbfa55 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,7 +1,13 @@
-%ul.nav-links
- = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
- = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
- Your Snippets
- = nav_link(page: explore_snippets_path) do
- = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
- Explore Snippets
+.top-area
+ %ul.nav-links
+ = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
+ = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
+ Your Snippets
+ = nav_link(page: explore_snippets_path) do
+ = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
+ Explore Snippets
+
+ - if current_user
+ .nav-controls.hidden-xs
+ = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
+ New snippet
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index fdea834ff45..1bbd4602ecf 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -4,6 +4,18 @@
Welcome to GitLab
%p.blank-state-text
Code, test, and deploy together
+
+- if current_user.can_create_group?
+ .blank-state
+ .blank-state-icon
+ = custom_icon("group", size: 50)
+ %h3.blank-state-title
+ You can create a group for several dependent projects.
+ %p.blank-state-text
+ Groups are the best way to manage projects and members.
+ = link_to new_group_path, class: "btn btn-new" do
+ New group
+
.blank-state
.blank-state-icon
= custom_icon("project", size: 50)
@@ -21,18 +33,7 @@
= link_to new_project_path, class: "btn btn-new" do
New project
-- if current_user.can_create_group?
- .blank-state
- .blank-state-icon
- = custom_icon("group", size: 50)
- %h3.blank-state-title
- You can create a group for several dependent projects.
- %p.blank-state-text
- Groups are the best way to manage projects and members.
- = link_to new_group_path, class: "btn btn-new" do
- New group
-
--if publicish_project_count > 0
+- if publicish_project_count > 0
.blank-state
.blank-state-icon
= icon("globe")
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index b2af438ea57..85cbe0bf0e6 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -2,41 +2,11 @@
- header_title "Snippets", dashboard_snippets_path
= render 'dashboard/snippets_head'
+= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
-.nav-block
- .controls.hidden-xs
- = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
- = icon('plus')
- New snippet
+.visible-xs
+ &nbsp;
+ = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
+ New snippet
- .nav-links.snippet-scope-menu
- %li{ class: ("active" unless params[:scope]) }
- = link_to dashboard_snippets_path do
- All
- %span.badge
- = current_user.snippets.count
-
- %li{ class: ("active" if params[:scope] == "are_private") }
- = link_to dashboard_snippets_path(scope: 'are_private') do
- Private
- %span.badge
- = current_user.snippets.are_private.count
-
- %li{ class: ("active" if params[:scope] == "are_internal") }
- = link_to dashboard_snippets_path(scope: 'are_internal') do
- Internal
- %span.badge
- = current_user.snippets.are_internal.count
-
- %li{ class: ("active" if params[:scope] == "are_public") }
- = link_to dashboard_snippets_path(scope: 'are_public') do
- Public
- %span.badge
- = current_user.snippets.are_public.count
-
- .visible-xs
- = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
- = icon('plus')
- New snippet
-
-= render 'snippets/snippets'
+= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index cc077fad32a..9d7bcdb9d16 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,9 +1,9 @@
-%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
+%li{ class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } }
= author_avatar(todo, size: 40)
.todo-item.todo-block
.todo-title.title
- - unless todo.build_failed?
+ - unless todo.build_failed? || todo.unmergeable?
= todo_target_state_pill(todo)
%span.author-name
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 5b2465e25ee..f4efcfb27b2 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -5,14 +5,14 @@
.top-area
%ul.nav-links
- todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
- %li{class: "todos-pending #{todo_pending_active}"}
+ %li{ class: "todos-pending #{todo_pending_active}" }>
= link_to todos_filter_path(state: 'pending') do
%span
To do
%span.badge
= number_with_delimiter(todos_pending_count)
- todo_done_active = ('active' if params[:state] == 'done')
- %li{class: "todos-done #{todo_done_active}"}
+ %li{ class: "todos-done #{todo_done_active}" }>
= link_to todos_filter_path(state: 'done') do
%span
Done
@@ -32,7 +32,7 @@
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
= dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options } })
+ placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } })
.filter-item.inline
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
@@ -42,21 +42,21 @@
- if params[:type].present?
= hidden_field_tag(:type, params[:type])
= dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
- data: { data: todo_types_options } })
+ data: { data: todo_types_options, default_label: 'Type' } })
.filter-item.inline.actions-filter
- if params[:action_id].present?
= hidden_field_tag(:action_id, params[:action_id])
= dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
- data: { data: todo_actions_options }})
+ data: { data: todo_actions_options, default_label: 'Action' } })
.pull-right
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
= link_to todos_filter_path(sort: sort_value_priority) do
@@ -84,7 +84,7 @@
= render "shared/empty_states/todos_all_done.svg"
- if todos_filter_empty?
%h4.text-center
- Good job! Looks like you don't have any todos left.
+ = Gitlab.config.gitlab.no_todos_messages.sample
%p.text-center
Are you looking for things to do? Take a look at
= succeed "," do
diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml
index 20cd7b0179d..fb70d158096 100644
--- a/app/views/devise/confirmations/almost_there.haml
+++ b/app/views/devise/confirmations/almost_there.haml
@@ -1,12 +1,13 @@
-.well-confirmation.text-center
+.well-confirmation.text-center.append-bottom-20
%h1.prepend-top-0
Almost there...
- %p.lead
+ %p.lead.append-bottom-20
Please check your email to confirm your account
+ %hr
- if current_application_settings.after_sign_up_text.present?
.well-confirmation.text-center
= markdown_field(current_application_settings, :after_sign_up_text)
-%p.confirmation-content.text-center
+%p.text-center
No confirmation email received? Please check your spam folder or
.append-bottom-20.prepend-top-20.text-center
%a.btn.btn-lg.btn-success{ href: new_user_confirmation_path }
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 21b89580818..5d359538efe 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,16 +1,16 @@
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
- %div.form-group
+ .form-group
= f.label "Username or email", for: :login
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
- %div.form-group
+ .form-group
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
- %div.submit-container.move-submit-down
- = f.submit "Sign in", class: "btn btn-save"
- if devise_mapping.rememberable?
.remember-me.checkbox
- %label{for: "user_remember_me"}
+ %label{ for: "user_remember_me" }
= f.check_box :remember_me
%span Remember me
.pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name)
+ .submit-container.move-submit-down
+ = f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index a6cadbcbdff..2556cb6f59b 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -1,13 +1,13 @@
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'gl-show-field-errors') do
.form-group
= label_tag :username, 'Username or email'
- = text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true }
+ = text_field_tag :username, nil, { class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true }
.form-group
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
.remember-me.checkbox
- %label{for: "remember_me"}
+ %label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
= submit_tag "Sign in", class: "btn-save btn"
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 3ab5461f929..3159d21598a 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,13 +1,13 @@
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
.form-group
= label_tag :username, "#{server['label']} Username"
- = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
+ = text_field_tag :username, nil, { class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
.form-group
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
.remember-me.checkbox
- %label{for: "remember_me"}
+ %label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
= submit_tag "Sign in", class: "btn-save btn"
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index fa8e7979461..af87129e49e 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,4 +1,5 @@
- page_title "Sign in"
+
%div
- if form_based_providers.any?
= render 'devise/shared/tabs_ldap'
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 2cadc424668..951f03083bf 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -7,7 +7,7 @@
.login-box
.login-body
- if @user.two_factor_otp_enabled?
- = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user gl-show-field-errors' }) do |f|
+ = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_u2f_enabled?}" }) do |f|
- resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 8908b64cdac..e87a16a5157 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,4 +1,4 @@
-%div.omniauth-container
+.omniauth-container
%p
%span.light
Sign in with &nbsp;
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 86edaf14e43..eddfce363a7 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -1,18 +1,18 @@
- if form_based_providers.any?
- if crowd_enabled?
- .login-box.tab-pane.active{id: "crowd", role: 'tabpanel', class: 'tab-pane'}
+ .login-box.tab-pane.active{ id: "crowd", role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_crowd'
- @ldap_servers.each_with_index do |server, i|
- .login-box.tab-pane{id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?)}
+ .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?) }
.login-body
= render 'devise/sessions/new_ldap', server: server
- if signin_enabled?
- .login-box.tab-pane{id: 'ldap-standard', role: 'tabpanel'}
+ .login-box.tab-pane{ id: 'ldap-standard', role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_base'
- elsif signin_enabled?
- .login-box.tab-pane.active{id: 'login-pane', role: 'tabpanel'}
+ .login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_base'
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 7c68e3266e5..01ecf237925 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -1,20 +1,23 @@
-#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' }
+#register-pane.tab-pane.login-box{ role: 'tabpanel' }
.login-body
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
.devise-errors
= devise_error_messages!
- %div.form-group
+ .form-group
= f.label :name
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
- %div.username.form-group
+ .username.form-group
= f.label :username
- = f.text_field :username, class: "form-control middle", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.'
+ = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
- %div.form-group
+ .form-group
= f.label :email
= f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address."
+ .form-group
+ = f.label :email_confirmation
+ = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address."
.form-group.append-bottom-20#password-strength
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 1e957f0935f..8c4ad30c832 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -3,8 +3,11 @@
%li.active
= link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
- %li{class: (:active if i.zero? && !crowd_enabled?)}
+ %li{ class: (:active if i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled?
%li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
+ - if signin_enabled? && signup_enabled?
+ %li
+ = link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index 05246303fb6..c225d800a98 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -1,6 +1,6 @@
-%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'}
+%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
- %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in
+ %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if signin_enabled? && signup_enabled?
- %li{ role: 'presentation'}
- %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register
+ %li{ role: 'presentation' }
+ %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index 1411daeb4a6..2deadbeeceb 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -1,5 +1,5 @@
- expanded = local_assigns.fetch(:expanded, true)
-%tr.notes_holder{class: ('hide' unless expanded)}
+%tr.notes_holder{ class: ('hide' unless expanded) }
%td.notes_line{ colspan: 2 }
%td.notes_content
.content
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 077e8e64e5f..2bce2780484 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -1,9 +1,6 @@
- expanded = discussion.expanded?
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
- .timeline-icon
- = link_to user_path(discussion.author) do
- = image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
@@ -13,9 +10,7 @@
= icon("chevron-up")
- else
= icon("chevron-down")
-
Toggle discussion
-
= link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light
@@ -37,7 +32,6 @@
an outdated diff
= time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago")
-
= render "discussions/headline", discussion: discussion
.discussion-body.js-toggle-content{ class: ("hide" unless expanded) }
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
index 7ed09dd1a98..69bd416c4de 100644
--- a/app/views/discussions/_jump_to_next.html.haml
+++ b/app/views/discussions/_jump_to_next.html.haml
@@ -5,5 +5,5 @@
%button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
title: "Jump to next unresolved discussion",
"aria-label" => "Jump to next unresolved discussion",
- data: { container: "body" }}
+ data: { container: "body" } }
= custom_icon("next_discussion")
diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml
index f1072ce0feb..ef16b516e2c 100644
--- a/app/views/discussions/_parallel_diff_discussion.html.haml
+++ b/app/views/discussions/_parallel_diff_discussion.html.haml
@@ -1,9 +1,9 @@
- expanded = discussion_left.try(:expanded?) || discussion_right.try(:expanded?)
-%tr.notes_holder{class: ('hide' unless expanded)}
+%tr.notes_holder{ class: ('hide' unless expanded) }
- if discussion_left
%td.notes_line.old
%td.notes_content.parallel.old
- .content{class: ('hide' unless discussion_left.expanded?)}
+ .content{ class: ('hide' unless discussion_left.expanded?) }
= render "discussions/notes", discussion: discussion_left, line_type: 'old'
- else
%td.notes_line.old= ""
@@ -13,7 +13,7 @@
- if discussion_right
%td.notes_line.new
%td.notes_content.parallel.new
- .content{class: ('hide' unless discussion_right.expanded?)}
+ .content{ class: ('hide' unless discussion_right.expanded?) }
= render "discussions/notes", discussion: discussion_right, line_type: 'new'
- else
%td.notes_line.new= ""
diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml
index 001a711b1dd..84b4ce5b606 100644
--- a/app/views/doorkeeper/applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/applications/_delete_form.html.haml
@@ -1,6 +1,6 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag oauth_application_path(application) do
- %input{:name => "_method", :type => "hidden", :value => "delete"}/
+ %input{ :name => "_method", :type => "hidden", :value => "delete" }/
- if defined? small
= button_tag type: "submit", class: "btn btn-transparent", data: { confirm: "Are you sure?" } do
%span.sr-only
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index 5c98265727a..b3313c7c985 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -17,5 +17,9 @@
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
+ .form-group
+ = f.label :scopes, class: 'label-light'
+ = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
+
.prepend-top-default
= f.submit 'Save application', class: "btn btn-create"
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index 3998e66f40d..aa271150b07 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -31,7 +31,7 @@
%th.last-heading
%tbody
- @applications.each do |application|
- %tr{id: "application_#{application.id}"}
+ %tr{ id: "application_#{application.id}" }
%td= link_to application.name, oauth_application_path(application)
%td
- application.redirect_uri.split.each do |uri|
@@ -63,7 +63,7 @@
%tbody
- @authorized_apps.each do |app|
- token = app.authorized_tokens.order('created_at desc').first
- %tr{id: "application_#{app.id}"}
+ %tr{ id: "application_#{app.id}" }
%td= app.name
%td= token.created_at
%td= token.scopes
@@ -72,7 +72,7 @@
%tr
%td
Anonymous
- %div.help-block
+ .help-block
%em Authorization was granted by entering your username and password in the application.
%td= token.created_at
%td= token.scopes
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 47442b78d48..559de63d96d 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -2,7 +2,7 @@
%h3.page-title
Application: #{@application.name}
-.table-holder
+.table-holder.oauth-application-show
%table.table
%tr
%td
@@ -22,6 +22,9 @@
- @application.redirect_uri.split.each do |uri|
%div
%span.monospace= uri
+
+ = render "shared/tokens/scopes_list", token: @application
+
.form-actions
= link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/doorkeeper/authorizations/error.html.haml b/app/views/doorkeeper/authorizations/error.html.haml
index a4c607cea60..6117b00149f 100644
--- a/app/views/doorkeeper/authorizations/error.html.haml
+++ b/app/views/doorkeeper/authorizations/error.html.haml
@@ -1,3 +1,3 @@
%h3.page-title An error has occurred
-%main{:role => "main"}
+%main{ :role => "main" }
%pre= @pre_auth.error_response.body[:error_description]
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index ce050007204..2a0e301c8dd 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -1,5 +1,5 @@
%h3.page-title Authorization required
-%main{:role => "main"}
+%main{ :role => "main" }
%p.h4
Authorize
%strong.text-info= @pre_auth.client.name
diff --git a/app/views/doorkeeper/authorizations/show.html.haml b/app/views/doorkeeper/authorizations/show.html.haml
index 01f9e46f142..44e868e6782 100644
--- a/app/views/doorkeeper/authorizations/show.html.haml
+++ b/app/views/doorkeeper/authorizations/show.html.haml
@@ -1,3 +1,3 @@
%h3.page-title Authorization code:
-%main{:role => "main"}
+%main{ :role => "main" }
%code#authorization_code= params[:code]
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index 9f02a8d2ed9..11c1e67878e 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -3,7 +3,7 @@
- path = oauth_authorized_application_path(0, token_id: token)
- else
- path = oauth_authorized_application_path(application)
-
+
= form_tag path do
- %input{:name => "_method", :type => "hidden", :value => "delete"}/
+ %input{ :name => "_method", :type => "hidden", :value => "delete" }/
= submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-remove btn-sm'
diff --git a/app/views/doorkeeper/authorized_applications/index.html.haml b/app/views/doorkeeper/authorized_applications/index.html.haml
index b184b9c01d4..c8a585560a2 100644
--- a/app/views/doorkeeper/authorized_applications/index.html.haml
+++ b/app/views/doorkeeper/authorized_applications/index.html.haml
@@ -1,6 +1,6 @@
%header.page-header
%h1 Your authorized applications
-%main{:role => "main"}
+%main{ :role => "main" }
.table-holder
%table.table.table-striped
%thead
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
index 790d90ad3ee..49bd9acd2db 100644
--- a/app/views/emojis/index.html.haml
+++ b/app/views/emojis/index.html.haml
@@ -7,5 +7,5 @@
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji|
%li.pull-left.text-center.emoji-menu-list-item
- %button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"}
+ %button.emoji-menu-btn.text-center.js-emoji-btn{ type: "button" }
= emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index c034bbe430e..a97cbd4d4b3 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -1,6 +1,9 @@
-- page_title "Access Denied"
-%h1 403
-%h3 Access Denied
-%hr
-%p You are not allowed to access this page.
-%p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"}
+- content_for(:title, 'Access Denied')
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
+ %h1
+ 403
+.container
+ %h3 Access Denied
+ %hr
+ %p You are not allowed to access this page.
+ %p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"}
diff --git a/app/views/errors/encoding.html.haml b/app/views/errors/encoding.html.haml
index 90cfbebfcc6..64f7f8e0836 100644
--- a/app/views/errors/encoding.html.haml
+++ b/app/views/errors/encoding.html.haml
@@ -1,5 +1,8 @@
-- page_title "Encoding Error"
-%h1 500
-%h3 Encoding Error
-%hr
-%p Page can't be loaded because of an encoding error.
+- content_for(:title, 'Encoding Error')
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
+ %h1
+ 500
+.container
+ %h3 Encoding Error
+ %hr
+ %p Page can't be loaded because of an encoding error.
diff --git a/app/views/errors/git_not_found.html.haml b/app/views/errors/git_not_found.html.haml
index ff5d4cc1506..d860957665b 100644
--- a/app/views/errors/git_not_found.html.haml
+++ b/app/views/errors/git_not_found.html.haml
@@ -1,7 +1,9 @@
-- page_title "Git Resource Not Found"
-%h1 404
-%h3 Git Resource Not found
-%hr
-%p
- Application can't get access to some branch or commit in your repository. It
- may have been moved.
+- content_for(:title, 'Git Resource Not Found')
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
+ %h1
+ 404
+.container
+ %h3 Git Resource Not found
+ %hr
+ %p Application can't get access to some branch or commit in your repository. It
+ may have been moved
diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml
index 3756b98ebb2..a0b9a632e22 100644
--- a/app/views/errors/not_found.html.haml
+++ b/app/views/errors/not_found.html.haml
@@ -1,5 +1,8 @@
-- page_title "Not Found"
-%h1 404
-%h3 The resource you were looking for doesn't exist.
-%hr
-%p You may have mistyped the address or the page may have moved.
+- content_for(:title, 'Not Found')
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
+ %h1
+ 404
+.container
+ %h3 The resource you were looking for doesn't exist.
+ %hr
+ %p You may have mistyped the address or the page may have moved.
diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml
index 3e70e98a24c..72508b91134 100644
--- a/app/views/errors/omniauth_error.html.haml
+++ b/app/views/errors/omniauth_error.html.haml
@@ -1,9 +1,12 @@
-- page_title "Auth Error"
-%h1 422
-%h3 Sign-in using #{@provider} auth failed
-%hr
-%p Sign-in failed because #{@error}.
-%p There are couple of steps you can take:
+- content_for(:title, 'Auth Error')
+%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') }
+ %h1
+ 422
+.container
+ %h3 Sign-in using #{@provider} auth failed
+ %hr
+ %p Sign-in failed because #{@error}.
+ %p There are couple of steps you can take:
%ul
%li Try logging in using your email
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 5c318cd3b8b..a0bd14df209 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -3,14 +3,13 @@
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
- = cache [event, current_application_settings, "v2.2"] do
- = author_avatar(event, size: 40)
+ = author_avatar(event, size: 40)
- - if event.created_project?
- = render "events/event/created_project", event: event
- - elsif event.push?
- = render "events/event/push", event: event
- - elsif event.commented?
- = render "events/event/note", event: event
- - else
- = render "events/event/common", event: event
+ - if event.created_project?
+ = render "events/event/created_project", event: event
+ - elsif event.push?
+ = render "events/event/push", event: event
+ - elsif event.commented?
+ = render "events/event/note", event: event
+ - else
+ = render "events/event/common", event: event
diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml
index 083c3936212..51585314a62 100644
--- a/app/views/events/_event_issue.atom.haml
+++ b/app/views/events/_event_issue.atom.haml
@@ -1,2 +1,2 @@
-%div{xmlns: "http://www.w3.org/1999/xhtml"}
+%div{ xmlns: "http://www.w3.org/1999/xhtml" }
= markdown(issue.description, pipeline: :atom, project: issue.project, author: issue.author)
diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml
index d7e05600627..56fc8b86217 100644
--- a/app/views/events/_event_merge_request.atom.haml
+++ b/app/views/events/_event_merge_request.atom.haml
@@ -1,2 +1,2 @@
-%div{xmlns: "http://www.w3.org/1999/xhtml"}
+%div{ xmlns: "http://www.w3.org/1999/xhtml" }
= markdown(merge_request.description, pipeline: :atom, project: merge_request.project, author: merge_request.author)
diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml
index 1154f982821..6fa2f9bd4db 100644
--- a/app/views/events/_event_note.atom.haml
+++ b/app/views/events/_event_note.atom.haml
@@ -1,2 +1,2 @@
-%div{xmlns: "http://www.w3.org/1999/xhtml"}
+%div{ xmlns: "http://www.w3.org/1999/xhtml" }
= markdown(note.note, pipeline: :atom, project: note.project, author: note.author)
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index 28bee1d0a33..f8f0bcb7608 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -1,4 +1,4 @@
-%div{xmlns: "http://www.w3.org/1999/xhtml"}
+%div{ xmlns: "http://www.w3.org/1999/xhtml" }
- event.commits.first(15).each do |commit|
%p
%strong= commit[:author][:name]
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index bba6e0d2c20..2fb6b5647da 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -1,6 +1,6 @@
.event-title
%span.author_name= link_to_author event
- %span{class: event.action_name}
+ %span{ class: event.action_name }
- if event.target
= event.action_name
%strong
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index aba64dd17d0..80cf2344fe1 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -1,6 +1,6 @@
.event-title
%span.author_name= link_to_author event
- %span{class: event.action_name}
+ %span{ class: event.action_name }
= event_action_name(event)
- if event.project
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 44fff49d99c..64ca3c32e01 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -18,7 +18,7 @@
- few_commits.each do |commit|
= render "events/commit", commit: commit, project: project, event: event
- - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project)
+ - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
- if event.commits_count > 1
%li.commits-stat
- if event.commits_count > 2
@@ -35,12 +35,12 @@
Compare #{from_label}...#{truncate_sha(event.commit_to)}
- if create_mr
- %span{"data-user-is" => event.author_id, "data-display" => "inline"}
+ %span
or
= link_to create_mr_path(project.default_branch, event.ref_name, project) do
create a merge request
- elsif create_mr
- %li.commits-stat{"data-user-is" => event.author_id}
+ %li.commits-stat
= link_to create_mr_path(project.default_branch, event.ref_name, project) do
Create Merge Request
- elsif event.rm_ref?
diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml
index d8a57560788..a3b0709e261 100644
--- a/app/views/explore/_head.html.haml
+++ b/app/views/explore/_head.html.haml
@@ -1,5 +1,5 @@
-.explore-title
- %h3
+.explore-title.text-center
+ %h2
Explore GitLab
%p.lead
Discover projects, groups and snippets. Share your projects with others
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index a1b39d9e1a0..73cf6e87eb4 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -17,13 +17,13 @@
.pull-right
.dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_groups_path(sort: sort_value_recently_created) do
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 4cff14b096b..e3088848492 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,13 +1,13 @@
- if current_user
.dropdown
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
= icon('globe')
%span.light Visibility:
- if params[:visibility_level].present?
= visibility_level_label(params[:visibility_level].to_i)
- else
Any
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(visibility_level: nil) do
@@ -20,14 +20,14 @@
- if @tags.present?
.dropdown
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
= icon('tags')
%span.light Tags:
- if params[:tag].present?
= params[:tag]
- else
Any
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(tag: nil) do
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 7def9eacdc9..e5706d04736 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -6,12 +6,4 @@
- else
= render 'explore/head'
-.row-content-block
- - if current_user
- = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
- New snippet
-
- .oneline
- Public snippets created by you and other users are listed here
-
-= render 'snippets/snippets'
+= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/groups/_group_lfs_settings.html.haml b/app/views/groups/_group_lfs_settings.html.haml
index af57065f0fc..3c622ca5c3c 100644
--- a/app/views/groups/_group_lfs_settings.html.haml
+++ b/app/views/groups/_group_lfs_settings.html.haml
@@ -8,4 +8,4 @@
Allow projects within this group to use Git LFS
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
%br/
- %span.descr This setting can be overridden in each project. \ No newline at end of file
+ %span.descr This setting can be overridden in each project.
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index ebf9aca7700..bc5d3c797ac 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -21,6 +21,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
+ = render 'shared/members/sort_dropdown'
.panel.panel-default
.panel-heading
Users with access to
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index de8f53b6b52..9d05bff6c4e 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
$("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}"));
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index dc6c1bb69de..b4aa4f24d9e 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -3,24 +3,27 @@
- if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues")
-.top-area
- = render 'shared/issuable/nav', type: :issues
- .nav-controls
+- if group_issues(@group).exists?
+ .top-area
+ = render 'shared/issuable/nav', type: :issues
- if current_user
- = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
- = icon('rss')
- %span.icon-label
- Subscribe
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
+ .nav-controls
+ = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
+ = icon('rss')
+ %span.icon-label
+ Subscribe
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
-= render 'shared/issuable/filter', type: :issues
+ = render 'shared/issuable/filter', type: :issues
-.row-content-block.second-block
- Only issues from
- %strong #{@group.name}
- group are listed here.
- - if current_user
- To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
+ .row-content-block.second-block
+ Only issues from the
+ %strong #{@group.name}
+ group are listed here.
+ - if current_user
+ To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
-.prepend-top-default
- = render 'shared/issues'
+ .prepend-top-default
+ = render 'shared/issues'
+- else
+ = render 'shared/empty_states/issues', project_select_button: true
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index e6953d94531..dbbdb583a24 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -2,8 +2,9 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- .nav-controls
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
+ - if current_user
+ .nav-controls
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index 23d438b2aa1..63cadfca530 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -34,21 +34,10 @@
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
= f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
- { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
+ { selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2'
- .col-md-6
- .form-group
- = f.label :due_date, "Due Date", class: "control-label"
- .col-sm-10
- = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
+ = render "shared/milestones/form_dates", f: f
.form-actions
= f.submit 'Create Milestone', class: "btn-create btn"
= link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel"
-
-
-:javascript
- $(".datepicker").datepicker({
- dateFormat: "yy-mm-dd",
- onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
- }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index d19eaa6add9..38d63fd9acc 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -21,5 +21,5 @@
= render 'shared/group_tips'
.form-actions
- = f.submit 'Create group', class: "btn btn-create", tabindex: 3
+ = f.submit 'Create group', class: "btn btn-create"
= link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 33fee334d93..2e7e5e5c309 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -18,8 +18,8 @@
.pull-right
- if project.archived
%span.label.label-warning archived
- %span.label.label-gray
- = repository_size(project)
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
= link_to 'Members', namespace_project_project_members_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
= link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove"
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index b439b40a75a..d256d14609e 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -4,27 +4,25 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
-.cover-block.groups-cover-block
+.group-home-panel.text-center
%div{ class: container_class }
.avatar-container.s70.group-avatar
= image_tag group_icon(@group), class: "avatar s70 avatar-tile"
- .group-info
- .cover-title
- %h1
- @#{@group.path}
- %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
- = visibility_level_icon(@group.visibility_level, fw: false)
-
- .group-right-buttons.btn-group
- - if current_user
- .pull-left.append-right-10= render 'shared/members/access_request_buttons', source: @group
- = render 'shared/notifications/button', notification_setting: @notification_setting
-
- - if @group.description.present?
- .cover-desc.description
- = markdown_field(@group, :description)
-
-%div.groups-header{ class: container_class }
+ %h1.group-title
+ @#{@group.path}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ = visibility_level_icon(@group.visibility_level, fw: false)
+
+ - if @group.description.present?
+ .group-home-desc
+ = markdown_field(@group, :description)
+
+ - if current_user
+ .group-buttons
+ = render 'shared/members/access_request_buttons', source: @group
+ = render 'shared/notifications/button', notification_setting: @notification_setting
+
+.groups-header{ class: container_class }
.top-area
%ul.nav-links
%li.active
@@ -34,6 +32,10 @@
%li
= link_to "#shared", 'data-toggle' => 'tab' do
Shared Projects
+ - if @nested_groups.present?
+ %li
+ = link_to "#groups", 'data-toggle' => 'tab' do
+ Subgroups
.nav-controls
= form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
@@ -49,3 +51,8 @@
- if @shared_projects.present?
.tab-pane#shared
= render "shared_projects", projects: @shared_projects
+
+ - if @nested_groups.present?
+ .tab-pane#groups
+ %ul.content-list
+ = render partial: 'shared/groups/group', collection: @nested_groups
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 65842a0479b..b74cc822295 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -1,8 +1,8 @@
-#modal-shortcuts.modal{tabindex: -1}
+#modal-shortcuts.modal{ tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h4
Keyboard Shortcuts
%small
@@ -82,7 +82,7 @@
.col-lg-4
%table.shortcut-mappings
- %tbody{ class: 'hidden-shortcut project', style: 'display:none' }
+ %tbody.hidden-shortcut.project{ style: 'display:none' }
%tr
%th
%th Global Dashboard
@@ -190,7 +190,7 @@
%td New issue
.col-lg-4
%table.shortcut-mappings
- %tbody{ class: 'hidden-shortcut network', style: 'display:none' }
+ %tbody.hidden-shortcut.network{ style: 'display:none' }
%tr
%th
%th Network Graph
@@ -240,7 +240,7 @@
.key
shift j
%td Scroll to bottom
- %tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
+ %tbody.hidden-shortcut.issues{ style: 'display:none' }
%tr
%th
%th Issues
@@ -264,7 +264,7 @@
%td.shortcut
.key l
%td Change Label
- %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
+ %tbody.hidden-shortcut.merge_requests{ style: 'display:none' }
%tr
%th
%th Merge Requests
diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml
index be257b51b9e..f6ebd76af9d 100644
--- a/app/views/help/show.html.haml
+++ b/app/views/help/show.html.haml
@@ -1,3 +1,3 @@
- page_title @path.split("/").reverse.map(&:humanize)
.documentation.wiki
- = markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com")
+ = markdown @markdown
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 070ed90da6d..dd1df46792b 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -182,7 +182,7 @@
.nav-controls
= text_field_tag 'sample', nil, class: 'form-control'
.dropdown
- %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span Sort by name
= icon('chevron-down')
%ul.dropdown-menu
@@ -205,121 +205,121 @@
%h2#buttons Buttons
.example
- %button.btn.btn-default{:type => "button"} Default
- %button.btn.btn-gray{:type => "button"} Gray
- %button.btn.btn-primary{:type => "button"} Primary
- %button.btn.btn-success{:type => "button"} Success
- %button.btn.btn-info{:type => "button"} Info
- %button.btn.btn-warning{:type => "button"} Warning
- %button.btn.btn-danger{:type => "button"} Danger
- %button.btn.btn-link{:type => "button"} Link
+ %button.btn.btn-default{ :type => "button" } Default
+ %button.btn.btn-gray{ :type => "button" } Gray
+ %button.btn.btn-primary{ :type => "button" } Primary
+ %button.btn.btn-success{ :type => "button" } Success
+ %button.btn.btn-info{ :type => "button" } Info
+ %button.btn.btn-warning{ :type => "button" } Warning
+ %button.btn.btn-danger{ :type => "button" } Danger
+ %button.btn.btn-link{ :type => "button" } Link
%h2#dropdowns Dropdowns
.example
.clearfix
.dropdown.inline.pull-left
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
%ul.dropdown-menu
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
.dropdown.inline.pull-right
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
.example
%div
.dropdown.inline
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-selectable
%li
- %a.is-active{href: "#"}
+ %a.is-active{ href: "#" }
Dropdown Option
.example
%div
.dropdown.inline
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Dropdown Title
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
= icon('search')
.dropdown-content
%ul
%li
- %a.is-active{href: "#"}
+ %a.is-active{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li.divider
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
.dropdown-footer
%strong Tip:
If an author is not a member of this project, you can still filter by his name while using the search field.
.dropdown.inline
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown loading
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
.dropdown-title
%span Dropdown Title
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
= icon('search')
.dropdown-content
%ul
%li
- %a.is-active{href: "#"}
+ %a.is-active{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li.divider
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
%li
- %a{href: "#"}
+ %a{ href: "#" }
Dropdown Option
.dropdown-footer
%strong Tip:
@@ -330,21 +330,21 @@
.example
%div
.dropdown.inline
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: {toggle: 'dropdown' } }
Dropdown user
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
.dropdown-title
%span Dropdown Title
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
= icon('search')
.dropdown-content
%ul
%li
- %a.dropdown-menu-user-link.is-active{href: "#"}
+ %a.dropdown-menu-user-link.is-active{ href: "#" }
= link_to_member_avatar(@user, size: 30)
%strong.dropdown-menu-user-full-name
= @user.name
@@ -354,24 +354,24 @@
.example
%div
.dropdown.inline
- %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown page 2
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two
.dropdown-page-one
.dropdown-title
- %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } }
= icon('arrow-left')
%span Dropdown Title
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
= icon('search')
.dropdown-content
%ul
%li
- %a.dropdown-menu-user-link.is-active{href: "#"}
+ %a.dropdown-menu-user-link.is-active{ href: "#" }
= link_to_member_avatar(@user, size: 30)
%strong.dropdown-menu-user-full-name
= @user.name
@@ -379,13 +379,13 @@
= @user.to_reference
.dropdown-page-two
.dropdown-title
- %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } }
= icon('arrow-left')
%span Create label
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Name new label"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Name new label" }
.dropdown-content
%button.btn.btn-primary
Create
@@ -393,16 +393,16 @@
.example
%div
.dropdown.inline
- %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ %button#js-project-dropdown.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Projects
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Go to project
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times')
.dropdown-input
- %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
= icon('search')
.dropdown-content
.dropdown-loading
@@ -486,22 +486,22 @@
.example
%form.form-horizontal
.form-group
- %label.col-sm-2.control-label{:for => "inputEmail3"} Email
+ %label.col-sm-2.control-label{ :for => "inputEmail3" } Email
.col-sm-10
- %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/
+ %input#inputEmail3.form-control{ :placeholder => "Email", :type => "email" }/
.form-group
- %label.col-sm-2.control-label{:for => "inputPassword3"} Password
+ %label.col-sm-2.control-label{ :for => "inputPassword3" } Password
.col-sm-10
- %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/
+ %input#inputPassword3.form-control{ :placeholder => "Password", :type => "password" }/
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
%label
- %input{:type => "checkbox"}/
+ %input{ :type => "checkbox" }/
Remember me
.form-group
.col-sm-offset-2.col-sm-10
- %button.btn.btn-default{:type => "submit"} Sign in
+ %button.btn.btn-default{ :type => "submit" } Sign in
.lead
Form when label rendered above input
@@ -510,16 +510,16 @@
.example
%form
.form-group
- %label{:for => "exampleInputEmail1"} Email address
- %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/
+ %label{ :for => "exampleInputEmail1" } Email address
+ %input#exampleInputEmail1.form-control{ :placeholder => "Enter email", :type => "email" }/
.form-group
- %label{:for => "exampleInputPassword1"} Password
- %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/
+ %label{ :for => "exampleInputPassword1" } Password
+ %input#exampleInputPassword1.form-control{ :placeholder => "Password", :type => "password" }/
.checkbox
%label
- %input{:type => "checkbox"}/
+ %input{ :type => "checkbox" }/
Remember me
- %button.btn.btn-default{:type => "submit"} Sign in
+ %button.btn.btn-default{ :type => "submit" } Sign in
%h2#file File
%h4
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
new file mode 100644
index 00000000000..864c5c0ff95
--- /dev/null
+++ b/app/views/import/_githubish_status.html.haml
@@ -0,0 +1,61 @@
+- provider = local_assigns.fetch(:provider)
+- provider_title = Gitlab::ImportSources.title(provider)
+
+%p.light
+ Select projects you want to import.
+%hr
+%p
+ = button_tag class: "btn btn-import btn-success js-import-all" do
+ Import all projects
+ = icon("spinner spin", class: "loading-icon")
+
+.table-responsive
+ %table.table.import-jobs
+ %colgroup.import-jobs-from-col
+ %colgroup.import-jobs-to-col
+ %colgroup.import-jobs-status-col
+ %thead
+ %tr
+ %th= "From #{provider_title}"
+ %th To GitLab
+ %th Status
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
+ %td
+ = provider_project_link(provider, project.import_source)
+ %td
+ = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span
+ %i.fa.fa-check
+ done
+ - elsif project.import_status == 'started'
+ %i.fa.fa-spinner.fa-spin
+ started
+ - else
+ = project.human_import_status_name
+
+ - @repos.each do |repo|
+ %tr{ id: "repo_#{repo.id}" }
+ %td
+ = provider_project_link(provider, repo.full_name)
+ %td.import-target
+ %fieldset.row
+ .input-group
+ .project-path.input-group-btn
+ - if current_user.can_select_namespace?
+ - selected = params[:namespace_id] || :current_user
+ - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+ - else
+ = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
+ %span.input-group-addon /
+ = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
+ %td.import-actions.job-status
+ = button_tag class: "btn btn-import js-add-to-import" do
+ Import
+ = icon("spinner spin", class: "loading-icon")
+
+.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", import_path: "#{url_for([:import, provider])}" } }
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index f8b4b107513..7f1b9ee7141 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,5 +1,6 @@
-- page_title "Bitbucket import"
-- header_title "Projects", root_path
+- page_title 'Bitbucket import'
+- header_title 'Projects', root_path
+
%h3.page-title
%i.fa.fa-bitbucket
Import projects from Bitbucket
@@ -10,13 +11,13 @@
%hr
%p
- if @incompatible_repos.any?
- = button_tag class: "btn btn-import btn-success js-import-all" do
+ = button_tag class: 'btn btn-import btn-success js-import-all' do
Import all compatible projects
- = icon("spinner spin", class: "loading-icon")
+ = icon('spinner spin', class: 'loading-icon')
- else
- = button_tag class: "btn btn-success js-import-all" do
+ = button_tag class: 'btn btn-import btn-success js-import-all' do
Import all projects
- = icon("spinner spin", class: "loading-icon")
+ = icon('spinner spin', class: 'loading-icon')
.table-responsive
%table.table.import-jobs
@@ -30,9 +31,9 @@
%th Status
%tbody
- @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
- = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank"
+ = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank'
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -47,31 +48,41 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+ %tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
%td
- = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
%td.import-target
- = import_project_target(repo['owner'], repo['slug'])
+ %fieldset.row
+ .input-group
+ .project-path.input-group-btn
+ - if current_user.can_select_namespace?
+ - selected = params[:namespace_id] || :current_user
+ - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {}
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+ - else
+ = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
+ %span.input-group-addon /
+ = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
- = button_tag class: "btn btn-import js-add-to-import" do
+ = button_tag class: 'btn btn-import js-add-to-import' do
Import
- = icon("spinner spin", class: "loading-icon")
+ = icon('spinner spin', class: 'loading-icon')
- @incompatible_repos.each do |repo|
- %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+ %tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
%td
- = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
%td.import-target
%td.import-actions-job-status
- = label_tag "Incompatible Project", nil, class: "label label-danger"
+ = label_tag 'Incompatible Project', nil, class: 'label label-danger'
- if @incompatible_repos.any?
%p
One or more of your Bitbucket projects cannot be imported into GitLab
directly because they use Subversion or Mercurial for version control,
rather than Git. Please convert
- = link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview"
+ = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview'
and go through the
- = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true"
+ = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true'
again.
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index c8a6fa1aa9e..97e5e51abe0 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -29,7 +29,7 @@
%th Status
%tbody
- @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
= project.import_source
%td
@@ -46,7 +46,7 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
+ %tr{ id: "repo_#{repo.id}" }
%td
= repo.name
%td.import-target
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
new file mode 100644
index 00000000000..02a116f996b
--- /dev/null
+++ b/app/views/import/gitea/new.html.haml
@@ -0,0 +1,23 @@
+- page_title "Gitea Import"
+- header_title "Projects", root_path
+
+%h3.page-title
+ = custom_icon('go_logo')
+ Import Projects from Gitea
+
+%p
+ To get started, please enter your Gitea Host URL and a
+ = succeed '.' do
+ = link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token'
+
+= form_tag personal_access_token_import_gitea_path, class: 'form-horizontal' do
+ .form-group
+ = label_tag :gitea_host_url, 'Gitea Host URL', class: 'control-label'
+ .col-sm-4
+ = text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control'
+ .form-group
+ = label_tag :personal_access_token, 'Personal Access Token', class: 'control-label'
+ .col-sm-4
+ = text_field_tag :personal_access_token, nil, class: 'form-control'
+ .form-actions
+ = submit_tag 'List Your Gitea Repositories', class: 'btn btn-create'
diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml
new file mode 100644
index 00000000000..589ca27e45d
--- /dev/null
+++ b/app/views/import/gitea/status.html.haml
@@ -0,0 +1,7 @@
+- page_title "Gitea Import"
+- header_title "Projects", root_path
+%h3.page-title
+ = custom_icon('go_logo')
+ Import Projects from Gitea
+
+= render 'import/githubish_status', provider: 'gitea'
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 4c721d40b55..0fe578a0036 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,64 +1,6 @@
-- page_title "GitHub import"
+- page_title "GitHub Import"
- header_title "Projects", root_path
%h3.page-title
- %i.fa.fa-github
- Import projects from GitHub
+ = icon 'github', text: 'Import Projects from GitHub'
-%p.light
- Select projects you want to import.
-%hr
-%p
- = button_tag class: "btn btn-import btn-success js-import-all" do
- Import all projects
- = icon("spinner spin", class: "loading-icon")
-
-.table-responsive
- %table.table.import-jobs
- %colgroup.import-jobs-from-col
- %colgroup.import-jobs-to-col
- %colgroup.import-jobs-status-col
- %thead
- %tr
- %th From GitHub
- %th To GitLab
- %th Status
- %tbody
- - @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
- %td
- = github_project_link(project.import_source)
- %td
- = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
- %td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- done
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- started
- - else
- = project.human_import_status_name
-
- - @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
- %td
- = github_project_link(repo.full_name)
- %td.import-target
- %fieldset.row
- .input-group
- .project-path.input-group-btn
- - if current_user.can_select_namespace?
- - selected = params[:namespace_id] || :current_user
- - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
- = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
- - else
- = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
- %span.input-group-addon /
- = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
- %td.import-actions.job-status
- = button_tag class: "btn btn-import js-add-to-import" do
- Import
- = icon("spinner spin", class: "loading-icon")
-
-.js-importer-status{ data: { jobs_import_path: "#{jobs_import_github_path}", import_path: "#{import_github_path}" } }
+= render 'import/githubish_status', provider: 'github'
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index d31fc2e6adb..d5b88709a34 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -24,7 +24,7 @@
%th Status
%tbody
- @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
= link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank"
%td
@@ -41,7 +41,7 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo["id"]}"}
+ %tr{ id: "repo_#{repo["id"]}" }
%td
= link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
%td.import-target
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index 5d2f149cd5f..336becd229e 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -45,7 +45,7 @@
%p
Upload <code>GoogleCodeProjectHosting.json</code> here:
%p
- %input{type: "file", name: "dump_file", id: "dump_file"}
+ %input{ type: "file", name: "dump_file", id: "dump_file" }
%li
%p
Do you want to customize how Google Code email addresses and usernames are imported into GitLab?
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index e79f122940a..9f1507cade6 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -34,7 +34,7 @@
%th Status
%tbody
- @already_added_projects.each do |project|
- %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
= link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
%td
@@ -51,7 +51,7 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
+ %tr{ id: "repo_#{repo.id}" }
%td
= link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
%td.import-target
@@ -61,7 +61,7 @@
Import
= icon("spinner spin", class: "loading-icon")
- @incompatible_repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
+ %tr{ id: "repo_#{repo.id}" }
%td
= link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
%td.import-target
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index 2fd4859c1c6..882fdf1317d 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -6,7 +6,7 @@
- if inviter = @member.created_by
by
= link_to inviter.name, user_url(inviter)
- to join
+ to join
- case @member.source
- when Project
- project = @member.source
@@ -20,11 +20,18 @@
= link_to group.name, group_url(group)
as #{@member.human_access}.
-- if @member.source.users.include?(current_user)
+- is_member = @member.source.users.include?(current_user)
+
+- if is_member
%p
However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}.
Sign in using a different account to accept the invitation.
-- else
+
+- if @member.invite_email != current_user.email
+ %p
+ Note that this invitation was sent to #{mail_to @member.invite_email}, but you are signed in as #{link_to current_user.to_reference, user_url(current_user)} with email #{mail_to current_user.email}.
+
+- unless is_member
.actions
= link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success"
= link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml
index 125f09777ba..c93dc7a50e8 100644
--- a/app/views/kaminari/gitlab/_next_page.html.haml
+++ b/app/views/kaminari/gitlab/_next_page.html.haml
@@ -6,8 +6,8 @@
-# per_page: number of items to fetch per page
-# remote: data-remote
- if current_page.last?
- %li{ class: "next disabled" }
+ %li.next.disabled
%span= raw(t 'views.pagination.next')
- else
- %li{ class: "next" }
+ %li.next
= link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index 750aed8f329..cefe0066a8f 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -6,5 +6,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li{class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}"}
- = link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil}
+%li{ class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}" }
+ = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil }
diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml
index f5e0d2ed3f3..8fe6bd653ae 100644
--- a/app/views/kaminari/gitlab/_paginator.html.haml
+++ b/app/views/kaminari/gitlab/_paginator.html.haml
@@ -6,7 +6,7 @@
-# remote: data-remote
-# paginator: the paginator that renders the pagination tags inside
= paginator.render do
- %div.gl-pagination
+ .gl-pagination
%ul.pagination.clearfix
- unless current_page.first?
= first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages
@@ -19,4 +19,3 @@
= next_page_tag
- unless current_page.last?
= last_page_tag unless total_pages < 5
-
diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml
index 7edf10498a8..b7c6caf7ff4 100644
--- a/app/views/kaminari/gitlab/_prev_page.html.haml
+++ b/app/views/kaminari/gitlab/_prev_page.html.haml
@@ -6,8 +6,8 @@
-# per_page: number of items to fetch per page
-# remote: data-remote
- if current_page.first?
- %li{ class: "prev disabled" }
+ %li.prev.disabled
%span= raw(t 'views.pagination.previous')
- else
- %li{ class: "prev" }
+ %li.prev
= link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 757de92d6d4..3096f0ee19e 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,27 +1,27 @@
- page_description brand_title unless page_description
- site_name = "GitLab"
-%head{prefix: "og: http://ogp.me/ns#"}
- %meta{charset: "utf-8"}
- %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
+%head{ prefix: "og: http://ogp.me/ns#" }
+ %meta{ charset: "utf-8" }
+ %meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
-# Open Graph - http://ogp.me/
- %meta{property: 'og:type', content: "object"}
- %meta{property: 'og:site_name', content: site_name}
- %meta{property: 'og:title', content: page_title}
- %meta{property: 'og:description', content: page_description}
- %meta{property: 'og:image', content: page_image}
- %meta{property: 'og:url', content: request.base_url + request.fullpath}
+ %meta{ property: 'og:type', content: "object" }
+ %meta{ property: 'og:site_name', content: site_name }
+ %meta{ property: 'og:title', content: page_title }
+ %meta{ property: 'og:description', content: page_description }
+ %meta{ property: 'og:image', content: page_image }
+ %meta{ property: 'og:url', content: request.base_url + request.fullpath }
-# Twitter Card - https://dev.twitter.com/cards/types/summary
- %meta{property: 'twitter:card', content: "summary"}
- %meta{property: 'twitter:title', content: page_title}
- %meta{property: 'twitter:description', content: page_description}
- %meta{property: 'twitter:image', content: page_image}
+ %meta{ property: 'twitter:card', content: "summary" }
+ %meta{ property: 'twitter:title', content: page_title }
+ %meta{ property: 'twitter:description', content: page_description }
+ %meta{ property: 'twitter:image', content: page_image }
= page_card_meta_tags
%title= page_title(site_name)
- %meta{name: "description", content: page_description}
+ %meta{ name: "description", content: page_description }
= favicon_link_tag 'favicon.ico'
@@ -36,25 +36,23 @@
= csrf_meta_tags
- unless browser.safari?
- %meta{name: 'referrer', content: 'origin-when-cross-origin'}
- %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'}
- %meta{name: 'theme-color', content: '#474D57'}
+ %meta{ name: 'referrer', content: 'origin-when-cross-origin' }
+ %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1' }
+ %meta{ name: 'theme-color', content: '#474D57' }
-# Apple Safari/iOS home screen icons
= favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon'
= favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76'
= favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120'
= favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152'
- %link{rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)'}
+ %link{ rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)' }
-# Windows 8 pinned site tile
- %meta{name: 'msapplication-TileImage', content: image_path('msapplication-tile.png')}
- %meta{name: 'msapplication-TileColor', content: '#30353E'}
+ %meta{ name: 'msapplication-TileImage', content: image_path('msapplication-tile.png') }
+ %meta{ name: 'msapplication-TileColor', content: '#30353E' }
= yield :meta_tags
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
= render 'layouts/bootlint' if Rails.env.development?
-
- = render 'layouts/user_styles'
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index e138ebab018..3daa1e90a8c 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -3,6 +3,14 @@
- if project
:javascript
- GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
- GitLab.GfmAutoComplete.cachedData = undefined;
- GitLab.GfmAutoComplete.setup();
+ gl.GfmAutoComplete.dataSources = {
+ emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
+ issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+ commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
+ };
+
+ gl.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 8aefdcb3d9b..54d02ee8e4b 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -22,9 +22,10 @@
= render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" }
= yield :sub_nav
- = render "layouts/broadcast"
- = render "layouts/flash"
- = yield :flash_message
+ .alert-wrapper
+ = render "layouts/broadcast"
+ = render "layouts/flash"
+ = yield :flash_message
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
- .content
+ .content{ id: "content-body" }
= yield
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 8e65bd12c56..0e64ebd71b8 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -6,7 +6,7 @@
- group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
- if @project && @project.persisted?
- project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: namespace_project_issues_path(@project.namespace, @project), mr_path: namespace_project_merge_requests_path(@project.namespace, @project) }
-.search.search-form{class: "#{'has-location-badge' if label.present?}"}
+.search.search-form{ class: "#{'has-location-badge' if label.present?}" }
= form_tag search_path, method: :get, class: 'navbar-form' do |f|
.search-input-container
- if label.present?
@@ -44,4 +44,4 @@
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
= button_tag 'Go' if ENV['RAILS_ENV'] == 'test'
- .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
+ .search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
diff --git a/app/views/layouts/_user_styles.html.haml b/app/views/layouts/_user_styles.html.haml
deleted file mode 100644
index b76b3cb5510..00000000000
--- a/app/views/layouts/_user_styles.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-:css
- [data-user-is] {
- display: none !important;
- }
-
- [data-user-is="#{current_user.try(:id)}"] {
- display: block !important;
- }
-
- [data-user-is="#{current_user.try(:id)}"][data-display="inline"] {
- display: inline !important;
- }
-
- [data-user-is-not] {
- display: block !important;
- }
-
- [data-user-is-not][data-display="inline"] {
- display: inline !important;
- }
-
- [data-user-is-not="#{current_user.try(:id)}"] {
- display: none !important;
- }
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 6c2285fa2b6..935517d4913 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: "en", class: "#{page_class}" }
= render "layouts/head"
- %body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}}
+ %body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= Gon::Base.render_data
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index afd9958f073..3368a9beb29 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,7 +1,7 @@
!!! 5
-%html{ lang: "en", class: "devise-layout-html"}
+%html.devise-layout-html
= render "layouts/head"
- %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page }}
+ %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap
= Gon::Base.render_data
= render "layouts/header/empty"
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 6bd427b02ac..7466423a934 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,5 +1,5 @@
!!! 5
-%html{ lang: "en"}
+%html{ lang: "en" }
= render "layouts/head"
%body.ui_charcoal.login-page.application.navless
= Gon::Base.render_data
diff --git a/app/views/layouts/devise_mailer.html.haml b/app/views/layouts/devise_mailer.html.haml
index c258eafdd51..e1e1f9ae516 100644
--- a/app/views/layouts/devise_mailer.html.haml
+++ b/app/views/layouts/devise_mailer.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html
%head
- %meta(content='text/html; charset=UTF-8' http-equiv='Content-Type')
+ %meta{ content: 'text/html; charset=UTF-8', 'http-equiv'=> 'Content-Type' }
= stylesheet_link_tag 'mailers/devise'
%body
@@ -9,7 +9,7 @@
%tr
%td
%table#header
- %td{valign: "top"}
+ %td{ valign: "top" }
= image_tag('mailers/gitlab_header_logo.png', id: 'logo', alt: 'GitLab Wordmark')
%table#body
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 7fbe065df00..6d9ec043590 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -1,10 +1,59 @@
!!! 5
-%html{ lang: "en"}
- = render "layouts/head"
- %body{class: "#{user_application_theme} application navless"}
- = Gon::Base.render_data
- = render "layouts/header/empty"
- .container.navless-container
- = render "layouts/flash"
- .error-page
- = yield
+%html{ lang: "en" }
+ %head
+ %meta{ :content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport" }
+ %title= yield(:title)
+ :css
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
+
+ hr {
+ max-width: 800px;
+ margin: 18px auto;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+
+ img {
+ max-width: 40vw;
+ display: block;
+ margin: 40px auto;
+ }
+
+ .container {
+ margin: auto 20px;
+ }
+
+ ul {
+ margin: auto;
+ text-align: left;
+ display:inline-block;
+ }
+%body
+ = yield
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 7a9859262f7..9ecc0d11c95 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,10 +1,11 @@
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
- %div{ class: "container-fluid" }
+ %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
+ .container-fluid
.header-content
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
%span.sr-only Toggle navigation
= icon('bars')
- %button.navbar-toggle{type: 'button'}
+ %button.navbar-toggle{ type: 'button' }
%span.sr-only Toggle navigation
= icon('ellipsis-v')
@@ -43,7 +44,7 @@
%li
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li
- = link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" }
+ = link_to "Settings", profile_path, aria: { label: "Settings" }
%li
= link_to "Help", help_path, aria: { label: "Help" }
%li.divider
diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml
index 38e9b80d129..9de0e12a826 100644
--- a/app/views/layouts/nav/_admin_settings.html.haml
+++ b/app/views/layouts/nav/_admin_settings.html.haml
@@ -1,6 +1,6 @@
.controls
.dropdown.admin-settings-dropdown
- %a.dropdown-new.btn.btn-default{href: '#', 'data-toggle' => 'dropdown'}
+ %a.dropdown-new.btn.btn-default{ href: '#', 'data-toggle' => 'dropdown' }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index a0356feef95..205d23178d2 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,5 +1,4 @@
.nav-sidebar
- .sidebar-header Across GitLab
%ul.nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
@@ -26,13 +25,17 @@
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
%span
Issues
- %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
+ %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
%span
Merge Requests
- %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
+ %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, title: 'Snippets' do
%span
Snippets
+
+ = link_to help_path, title: 'About GitLab CE', class: 'about-gitlab' do
+ %span
+ About GitLab CE
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index f7edb47b666..f3539fd372d 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -31,7 +31,7 @@
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
%span
Merge Requests
- - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute
+ - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.count= number_with_delimiter(merge_requests.count)
= nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index 75275afc0f3..30feb6813b4 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,26 +1,18 @@
- if current_user
- can_admin_group = can?(current_user, :admin_group, @group)
- can_edit = can?(current_user, :admin_group, @group)
- - member = @group.members.find_by(user_id: current_user.id)
- - can_leave = member && can?(current_user, :destroy_group_member, member)
- - if can_admin_group || can_edit || can_leave
+ - if can_admin_group || can_edit
.controls
.dropdown.group-settings-dropdown
- %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ %a.dropdown-new.btn.btn-default#group-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- if can_admin_group
= nav_link(path: 'groups#projects') do
= link_to 'Projects', projects_group_path(@group), title: 'Projects'
- - if can_edit || can_leave
+ - if can_edit && can_admin_group
%li.divider
- - if can_edit
%li
= link_to 'Edit Group', edit_group_path(@group)
- - if can_leave
- %li
- = link_to polymorphic_path([:leave, @group, :members]),
- data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
- Leave Group
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 6d514f669db..e06301bda14 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -17,6 +17,10 @@
= link_to applications_profile_path, title: 'Applications' do
%span
Applications
+ = nav_link(controller: :chat_names) do
+ = link_to profile_chat_names_path, title: 'Chat' do
+ %span
+ Chat
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 99a58bbb676..a8bbd67de80 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,28 +1,19 @@
- if current_user
.controls
.dropdown.project-settings-dropdown
- %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- can_edit = can?(current_user, :admin_project, @project)
- -# We don't use @project.team.find_member because it searches for group members too...
- - member = @project.members.find_by(user_id: current_user.id)
- - can_leave = member && can?(current_user, :destroy_project_member, member)
= render 'layouts/nav/project_settings', can_edit: can_edit
- - if can_edit || can_leave
+ - if can_edit
%li.divider
- - if can_edit
- %li
- = link_to edit_project_path(@project) do
- Edit Project
- - if can_leave
- %li
- = link_to polymorphic_path([:leave, @project, :members]),
- data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
- Leave Project
+ %li
+ = link_to edit_project_path(@project) do
+ Edit Project
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
@@ -70,14 +61,14 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
+ %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
Merge Requests
- %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
+ %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 613b8b7d301..c6df66d2c3c 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -1,26 +1,17 @@
- if project_nav_tab? :team
- = nav_link(controller: [:project_members, :teams]) do
- = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
+ = nav_link(controller: [:members, :teams]) do
+ = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
Members
- if can_edit
- - if @project.allowed_to_share_with_group?
- = nav_link(controller: :group_links) do
- = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
- %span
- Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span
Deploy Keys
- = nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
- %span
- Webhooks
- = nav_link(controller: :services) do
- = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
+ = nav_link(controller: :integrations) do
+ = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
%span
- Services
+ Integrations
= nav_link(controller: :protected_branches) do
= link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
%span
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 1ec4c3f0c67..76268c1b705 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -1,14 +1,14 @@
-%html{lang: "en"}
+%html{ lang: "en" }
%head
- %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"}
+ %meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
%title
GitLab
= stylesheet_link_tag 'notify'
= yield :head
%body
- %div.content
+ .content
= yield
- %div.footer{style: "margin-top: 10px;"}
+ .footer{ style: "margin-top: 10px;" }
%p
&mdash;
%br
diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml
index b77d3402a2e..0ee8a57dbd4 100644
--- a/app/views/layouts/profile.html.haml
+++ b/app/views/layouts/profile.html.haml
@@ -1,5 +1,5 @@
-- page_title "Profile Settings"
-- header_title "Profile Settings", profile_path unless header_title
+- page_title "User Settings"
+- header_title "User Settings", profile_path unless header_title
- sidebar "dashboard"
- nav "profile"
diff --git a/app/views/notify/_note_message.text.erb b/app/views/notify/_note_message.text.erb
new file mode 100644
index 00000000000..f82cbc9a3fc
--- /dev/null
+++ b/app/views/notify/_note_message.text.erb
@@ -0,0 +1,5 @@
+<% if current_application_settings.email_author_in_body %>
+ <%= @note.author_name %> wrote:
+<% end -%>
+
+<%= @note.note %>
diff --git a/app/views/notify/_note_mr_or_commit_email.html.haml b/app/views/notify/_note_mr_or_commit_email.html.haml
new file mode 100644
index 00000000000..edf8dfe7e9e
--- /dev/null
+++ b/app/views/notify/_note_mr_or_commit_email.html.haml
@@ -0,0 +1,18 @@
+= content_for :head do
+ = stylesheet_link_tag 'mailers/highlighted_diff_email'
+
+New comment
+
+- if @discussion && @discussion.diff_file
+ on
+ = link_to @note.diff_file.file_path, @target_url, class: 'details'
+ \:
+ %table
+ = render partial: "projects/diffs/line",
+ collection: @discussion.truncated_diff_lines,
+ as: :line,
+ locals: { diff_file: @note.diff_file,
+ plain: true,
+ email: true }
+
+= render 'note_message'
diff --git a/app/views/notify/_note_mr_or_commit_email.text.erb b/app/views/notify/_note_mr_or_commit_email.text.erb
new file mode 100644
index 00000000000..b4fcdf6b1e9
--- /dev/null
+++ b/app/views/notify/_note_mr_or_commit_email.text.erb
@@ -0,0 +1,8 @@
+<% if @discussion && @discussion.diff_file -%>
+ on <%= @note.diff_file.file_path -%>
+<% end -%>:
+
+<%= url %>
+
+<%= render 'simple_diff' if @discussion -%>
+<%= render 'note_message' %>
diff --git a/app/views/notify/_simple_diff.text.erb b/app/views/notify/_simple_diff.text.erb
new file mode 100644
index 00000000000..c28d1cc34d3
--- /dev/null
+++ b/app/views/notify/_simple_diff.text.erb
@@ -0,0 +1,3 @@
+<% @discussion.truncated_diff_lines(highlight: false).each do |line| %>
+> <%= line.text %>
+<% end %>
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
index 4bf7c1f4d64..a744c4be9d6 100644
--- a/app/views/notify/build_fail_email.html.haml
+++ b/app/views/notify/build_fail_email.html.haml
@@ -1,5 +1,5 @@
- content_for :header do
- %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
+ %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
GitLab (build failed)
%h3
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
index 252a5b7152c..8c2e6db1426 100644
--- a/app/views/notify/build_success_email.html.haml
+++ b/app/views/notify/build_success_email.html.haml
@@ -1,5 +1,5 @@
- content_for :header do
- %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
+ %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
GitLab (build successful)
%h3
diff --git a/app/views/notify/links/ci/builds/_build.html.haml b/app/views/notify/links/ci/builds/_build.html.haml
new file mode 100644
index 00000000000..d35b3839171
--- /dev/null
+++ b/app/views/notify/links/ci/builds/_build.html.haml
@@ -0,0 +1,2 @@
+%a{ href: pipeline_build_url(pipeline, build), style: "color:#3777b0;text-decoration:none;" }
+ = build.name
diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb
new file mode 100644
index 00000000000..f495a2e5486
--- /dev/null
+++ b/app/views/notify/links/ci/builds/_build.text.erb
@@ -0,0 +1 @@
+Build #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> )
diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml
new file mode 100644
index 00000000000..b6563b185b3
--- /dev/null
+++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.html.haml
@@ -0,0 +1 @@
+= build.name
diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
new file mode 100644
index 00000000000..8e89c52a1f3
--- /dev/null
+++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
@@ -0,0 +1 @@
+Build #<%= build.id %>
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index f42b150c0d6..d1855568215 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -1,7 +1,7 @@
- if current_application_settings.email_author_in_body
%div
#{link_to @issue.author_name, user_url(@issue.author)} wrote:
--if @issue.description
+- if @issue.description
= markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignee_id.present?
diff --git a/app/views/notify/new_mention_in_issue_email.html.haml b/app/views/notify/new_mention_in_issue_email.html.haml
index 4f3d36bd9ca..02f21baa368 100644
--- a/app/views/notify/new_mention_in_issue_email.html.haml
+++ b/app/views/notify/new_mention_in_issue_email.html.haml
@@ -4,7 +4,7 @@
- if current_application_settings.email_author_in_body
%div
#{link_to @issue.author_name, user_url(@issue.author)} wrote:
--if @issue.description
+- if @issue.description
= markdown(@issue.description, pipeline: :email, author: @issue.author)
- if @issue.assignee_id.present?
diff --git a/app/views/notify/new_mention_in_merge_request_email.html.haml b/app/views/notify/new_mention_in_merge_request_email.html.haml
index 32aedb9e6b9..cbd434be02a 100644
--- a/app/views/notify/new_mention_in_merge_request_email.html.haml
+++ b/app/views/notify/new_mention_in_merge_request_email.html.haml
@@ -11,5 +11,5 @@
%p
Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
--if @merge_request.description
+- if @merge_request.description
= markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 158404de396..8890b300f7d 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -8,5 +8,5 @@
%p
Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
--if @merge_request.description
+- if @merge_request.description
= markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/note_commit_email.html.haml b/app/views/notify/note_commit_email.html.haml
index 1d961e4424c..0a650e3b2ca 100644
--- a/app/views/notify/note_commit_email.html.haml
+++ b/app/views/notify/note_commit_email.html.haml
@@ -1,2 +1,2 @@
-= render 'note_message'
-
+%p.details
+ = render 'note_mr_or_commit_email'
diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb
index aaeaf5fdf73..6aa085a172e 100644
--- a/app/views/notify/note_commit_email.text.erb
+++ b/app/views/notify/note_commit_email.text.erb
@@ -1,9 +1,2 @@
-New comment for Commit <%= @commit.short_id %>
-
-<%= url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %>
-
-
-Author: <%= @note.author_name %>
-
-<%= @note.note %>
-
+New comment for Commit <%= @commit.short_id -%>
+<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url } %>
diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml
index ea7e3d199fd..0a650e3b2ca 100644
--- a/app/views/notify/note_merge_request_email.html.haml
+++ b/app/views/notify/note_merge_request_email.html.haml
@@ -1,7 +1,2 @@
-- if @note.diff_note? && @note.diff_file
- %p.details
- New comment on diff for
- = link_to @note.diff_file.file_path, @target_url
- \:
-
-= render 'note_message'
+%p.details
+ = render 'note_mr_or_commit_email'
diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb
index 8cdab63829e..2ce64c494cf 100644
--- a/app/views/notify/note_merge_request_email.text.erb
+++ b/app/views/notify/note_merge_request_email.text.erb
@@ -1,9 +1,2 @@
-New comment for Merge Request <%= @merge_request.to_reference %>
-
-<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %>
-
-
-<%= @note.author_name %>
-
-<%= @note.note %>
-
+New comment for Merge Request <%= @merge_request.to_reference -%>
+<%= render partial: 'note_mr_or_commit_email', locals: { url: @target_url }%>
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index 38c852f0a3a..82c7fe229b8 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -1,9 +1,9 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-%html{lang: "en"}
+%html{ lang: "en" }
%head
- %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
- %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
- %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/
+ %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
+ %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
+ %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
@@ -41,137 +41,139 @@
padding-right: 10px !important;
}
}
- %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
- %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"}
+ %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
%tr.line
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
%tr.header
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
- %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
- %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
- %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
%tbody
%tr.alert
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"}
- %img{alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+ %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
Your pipeline has failed.
%tr.spacer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.section
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
- %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
- %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
= namespace_name
\/
- %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
= @project.name
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha
- if @merge_request
in
- %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"}
+ %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
= @merge_request.to_reference
- .commit{style: "color:#5c5c5c;font-weight:300;"}
+ .commit{ style: "color:#5c5c5c;font-weight:300;" }
= @pipeline.git_commit_message.truncate(50)
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- commit = @pipeline.commit
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
- %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
= commit.author.name
- else
%span
= commit.author_name
%tr.spacer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
- failed = @pipeline.statuses.latest.failed
%tr.pre-section
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" }
Pipeline
- %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= "\##{@pipeline.id}"
had
= failed.size
failed
= "#{'build'.pluralize(failed.size)}."
%tr.warning
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
%tr.section
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;"}
- %table.builds{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" }
+ %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" }
%tbody
- failed.each do |build|
%tr.build-state
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;"}
- %img{alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" }
+ %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
= build.stage
- %td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
- %a{href: pipeline_build_url(@pipeline, build), style: "color:#3777b0;text-decoration:none;"}
- = build.name
+ %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
+ = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build
%tr.build-log
- %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"}
- %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"}
- = build.trace_html(last_lines: 10).html_safe
+ - if build.has_trace?
+ %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" }
+ %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" }
+ = build.trace_html(last_lines: 10).html_safe
+ - else
+ %td{ colspan: "2" }
%tr.footer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
- %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
%div
- %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications
+ %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
&middot;
- %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help
+ %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
%div
You're receiving this email because of your account on
= succeed "." do
- %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host
+ %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index 8f8084b58e1..ab91c7ef350 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -19,10 +19,12 @@ Commit Author: <%= commit.author_name %>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<% failed.each do |build| -%>
-Build #<%= build.id %> ( <%= pipeline_build_url(@pipeline, build) %> )
+<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
Stage: <%= build.stage %>
Name: <%= build.name %>
+<% if build.has_trace? -%>
Trace: <%= build.trace_with_state(last_lines: 10)[:text] %>
+<% end -%>
<% end -%>
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 697c8d19257..6dddb3b6373 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -1,9 +1,9 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-%html{lang: "en"}
+%html{ lang: "en" }
%head
- %meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
- %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
- %meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/
+ %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
+ %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
+ %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
@@ -41,114 +41,114 @@
padding-right: 10px !important;
}
}
- %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
- %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"}
+ %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
%tr.line
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}  
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  
%tr.header
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
- %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55" }/
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
- %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
- %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
%tbody
%tr.success
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"}
- %img{alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+ %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
Your pipeline has passed.
%tr.spacer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.section
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
- %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
- %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
= namespace_name
\/
- %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
= @project.name
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha
- if @merge_request
in
- %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"}
+ %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
= @merge_request.to_reference
- .commit{style: "color:#5c5c5c;font-weight:300;"}
+ .commit{ style: "color:#5c5c5c;font-weight:300;" }
= @pipeline.git_commit_message.truncate(50)
%tr
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
- %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- commit = @pipeline.commit
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
- %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
- %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"}
+ %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
= commit.author.name
- else
%span
= commit.author_name
%tr.spacer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.success-message
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"}
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" }
- build_count = @pipeline.statuses.latest.size
- - stage_count = @pipeline.stages.size
+ - stage_count = @pipeline.stages_count
Pipeline
- %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= "\##{@pipeline.id}"
successfully completed
= "#{build_count} #{'build'.pluralize(build_count)}"
in
= "#{stage_count} #{'stage'.pluralize(stage_count)}."
%tr.footer
- %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
- %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
%div
- %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications
+ %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
&middot;
- %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help
+ %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
%div
You're receiving this email because of your account on
= succeed "." do
- %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host
+ %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index ae22d474f2c..40e5e306426 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -16,7 +16,7 @@ Commit Author: <%= commit.author_name %>
<% end -%>
<% build_count = @pipeline.statuses.latest.size -%>
-<% stage_count = @pipeline.stages.size -%>
+<% stage_count = @pipeline.stages_count -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
diff --git a/app/views/notify/project_was_not_exported_email.text.haml b/app/views/notify/project_was_not_exported_email.text.haml
index b27cb620b9e..27785165c2d 100644
--- a/app/views/notify/project_was_not_exported_email.text.haml
+++ b/app/views/notify/project_was_not_exported_email.text.haml
@@ -3,4 +3,4 @@
= "The errors we encountered were:"
- @errors.each do |error|
- #{error} \ No newline at end of file
+ #{error}
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index c0c07d65daa..36858fa6f34 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -1,5 +1,5 @@
= content_for :head do
- = stylesheet_link_tag 'mailers/repository_push_email'
+ = stylesheet_link_tag 'mailers/highlighted_diff_email'
%h3
#{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name}
@@ -27,9 +27,9 @@
%h4 #{pluralize @message.diffs_count, "changed file"}:
%ul
- - @message.diffs.each_with_index do |diff, i|
+ - @message.diffs.each do |diff|
%li.file-stats
- %a{href: "#{@message.target_url if @message.disable_diffs?}#diff-#{i}" }
+ %a{ href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff.file_path)}" }
- if diff.deleted_file
%span.deleted-file
&minus;
@@ -52,9 +52,10 @@
%h5 The diff was not included because it is too large.
- else
%h4 Changes:
- - diff_files.each_with_index do |diff_file, i|
- %li{id: "diff-#{i}"}
- %a{href: @message.target_url + "#diff-#{i}"}<
+ - diff_files.each do |diff_file|
+ - file_hash = hexdigest(diff_file.file_path)
+ %li{ id: file_hash }
+ %a{ href: @message.target_url + "##{file_hash}" }<
- if diff_file.deleted_file
%strong<
= diff_file.old_path
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
new file mode 100644
index 00000000000..1ec1e7c70e4
--- /dev/null
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -0,0 +1,27 @@
+- service = chat_name.service
+- project = service.project
+%tr
+ %td
+ %strong
+ - if can?(current_user, :read_project, project)
+ = link_to project.name_with_namespace, project_path(project)
+ - else
+ .light N/A
+ %td
+ %strong
+ - if can?(current_user, :admin_project, project)
+ = link_to service.title, edit_namespace_project_service_path(project.namespace, project, service)
+ - else
+ = service.title
+ %td
+ = chat_name.team_domain
+ %td
+ = chat_name.chat_name
+ %td
+ - if chat_name.last_used_at
+ = time_ago_with_tooltip(chat_name.last_used_at)
+ - else
+ Never
+
+ %td
+ = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger pull-right', data: { confirm: 'Are you sure you want to revoke this nickname?' }
diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml
new file mode 100644
index 00000000000..20cc636b2da
--- /dev/null
+++ b/app/views/profiles/chat_names/index.html.haml
@@ -0,0 +1,30 @@
+- page_title 'Chat'
+= render 'profiles/head'
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ You can see your Chat accounts.
+
+ .col-lg-9
+ %h5 Active chat names (#{@chat_names.size})
+
+ - if @chat_names.present?
+ .table-responsive
+ %table.table.chat-names
+ %thead
+ %tr
+ %th Project
+ %th Service
+ %th Team domain
+ %th Nickname
+ %th Last used
+ %th
+ %tbody
+ = render @chat_names
+
+ - else
+ .settings-message.text-center
+ You don't have any active chat names.
diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml
new file mode 100644
index 00000000000..5bf22aa94f1
--- /dev/null
+++ b/app/views/profiles/chat_names/new.html.haml
@@ -0,0 +1,15 @@
+%h3.page-title Authorization required
+%main{ :role => "main" }
+ %p.h4
+ Authorize
+ %strong.text-info= @chat_name_params[:chat_name]
+ to use your account?
+
+ %hr
+ .actions
+ = form_tag profile_chat_names_path, method: :post do
+ = hidden_field_tag :token, @chat_name_token.token
+ = submit_tag "Authorize", class: "btn btn-success wide pull-left"
+ = form_tag deny_profile_chat_names_path, method: :delete do
+ = hidden_field_tag :token, @chat_name_token.token
+ = submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 3276db6692c..d2a60ac2867 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -6,6 +6,9 @@
= key.title
.description
= key.fingerprint
+ .last-used-at
+ last used:
+ = key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a'
.pull-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index dd7615400dc..d44603c638c 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -11,6 +11,9 @@
%li
%span.light Created on:
%strong= @key.created_at.to_s(:medium)
+ %li
+ %span.light Last used on:
+ %strong= @key.last_used_at.try(:to_s, :medium) || 'N/A'
.col-md-8
%p
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 93187873501..71b224a413b 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -17,5 +17,5 @@
%hr
%h5
Your SSH keys (#{@keys.count})
- %div.append-bottom-default
+ .append-bottom-default
= render 'key_table'
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 844fce59704..5c5e5940365 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -3,7 +3,7 @@
%div
- if @user.errors.any?
- %div.alert.alert-danger
+ .alert.alert-danger
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
@@ -30,7 +30,7 @@
%br
.clearfix
.form-group.pull-left.global-notification-setting
- = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
+ = render 'shared/notifications/button', notification_setting: @global_notification_setting
.clearfix
diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml
new file mode 100644
index 00000000000..3f6efa33953
--- /dev/null
+++ b/app/views/profiles/personal_access_tokens/_form.html.haml
@@ -0,0 +1,21 @@
+- personal_access_token = local_assigns.fetch(:personal_access_token)
+- scopes = local_assigns.fetch(:scopes)
+
+= form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f|
+
+ = form_errors(personal_access_token)
+
+ .form-group
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: "form-control", required: true
+
+ .form-group
+ = f.label :expires_at, class: 'label-light'
+ = f.text_field :expires_at, class: "datepicker form-control"
+
+ .form-group
+ = f.label :scopes, class: 'label-light'
+ = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes
+
+ .prepend-top-default
+ = f.submit 'Create Personal Access Token', class: "btn btn-create"
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 05a2ea67aa2..60a561c9f9c 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -19,7 +19,7 @@
Your New Personal Access Token
.form-group
= text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block"
- = clipboard_button(clipboard_text: flash[:personal_access_token])
+ = clipboard_button(clipboard_text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left")
%span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
%hr
@@ -28,21 +28,8 @@
Add a Personal Access Token
%p.profile-settings-content
Pick a name for the application, and we'll give you a unique token.
- = form_for [:profile, @personal_access_token],
- method: :post, html: { class: 'js-requires-input' } do |f|
- = form_errors(@personal_access_token)
-
- .form-group
- = f.label :name, class: 'label-light'
- = f.text_field :name, class: "form-control", required: true
-
- .form-group
- = f.label :expires_at, class: 'label-light'
- = f.text_field :expires_at, class: "datepicker form-control", required: false
-
- .prepend-top-default
- = f.submit 'Create Personal Access Token', class: "btn btn-create"
+ = render "form", personal_access_token: @personal_access_token, scopes: @scopes
%hr
@@ -56,6 +43,7 @@
%th Name
%th Created
%th Expires
+ %th Scopes
%th
%tbody
- @active_personal_access_tokens.each do |token|
@@ -67,6 +55,7 @@
= token.expires_at.to_date.to_s(:medium)
- else
%span.personal-access-tokens-never-expires-label Never
+ %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
%td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
- else
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 2afa026847a..feadd863b00 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,7 +1,7 @@
- page_title 'Preferences'
= render 'profiles/head'
-= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f|
+= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
Application theme
@@ -10,7 +10,7 @@
.col-lg-9.application-theme
- Gitlab::Themes.each do |theme|
= label_tag do
- .preview{class: theme.css_class}
+ .preview{ class: theme.css_class }
= f.radio_button :theme_id, theme.id
= theme.name
.col-sm-12
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 578af9fe98d..c0c82cde2f6 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -18,7 +18,8 @@
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
.col-lg-9
.clearfix.avatar-image.append-bottom-default
- = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
+ = link_to avatar_icon(@user, 400), target: '_blank' do
+ = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0
Upload new avatar
.prepend-top-5.append-bottom-10
@@ -30,7 +31,7 @@
The maximum file size allowed is 200KB.
- if @user.avatar?
%hr
- = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-gray"
+ = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?" }, method: :delete, class: "btn btn-gray"
%hr
.row
.col-lg-3.profile-settings-sidebar
@@ -69,7 +70,7 @@
%span.help-block We also use email for avatar detection if no avatar is uploaded.
.form-group
= f.label :public_email, class: "label-light"
- = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2"
+ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { include_blank: 'Do not show on profile' }, class: "select2"
%span.help-block This email will be displayed on your public profile.
.form-group
= f.label :skype, class: "label-light"
@@ -101,14 +102,14 @@
.modal-dialog
.modal-content
.modal-header
- %button.close{:type => "button", :'data-dismiss' => "modal"}
+ %button.close{ :type => "button", :'data-dismiss' => "modal" }
%span
&times;
%h4.modal-title
Position and size your new avatar
.modal-body
.profile-crop-image-container
- %img.modal-profile-crop-image
+ %img.modal-profile-crop-image{ alt: "Avatar cropper" }
.crop-controls
.btn-group
%button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } }
@@ -116,5 +117,5 @@
%button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } }
%span.fa.fa-search-minus
.modal-footer
- %button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
+ %button.btn.btn-primary.js-upload-user-avatar{ :type => "button" }
Set new profile picture
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 03ac739ade5..558a1d56151 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -30,7 +30,7 @@
To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0
Account:
- = current_user.email
+ = @account_string
%p.prepend-top-0.append-bottom-0
Key:
= current_user.otp_secret.scan(/.{4}/).join(' ')
diff --git a/app/views/profiles/update_username.js.haml b/app/views/profiles/update_username.js.haml
index de1337a2a24..5307e0b48cb 100644
--- a/app/views/profiles/update_username.js.haml
+++ b/app/views/profiles/update_username.js.haml
@@ -2,5 +2,6 @@
:plain
new Flash("Username successfully changed", "notice")
- else
+ - error = @user.errors.full_messages.first
:plain
- new Flash("Username change failed - #{@user.errors.full_messages.first}", "alert")
+ new Flash("Username change failed - #{escape_javascript error.html_safe}", "alert")
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index d011e51e696..0ea733cb978 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -9,11 +9,11 @@
= render 'shared/event_filter'
- .content_list.project-activity{:"data-href" => activity_project_path(@project)}
+ .content_list.project-activity{ :"data-href" => activity_project_path(@project) }
= spinner
:javascript
- var activity = new Activities();
+ var activity = new gl.Activities();
$(document).on('page:restore', function (event) {
activity.reloadActivities()
})
diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml
index e74fd5b93ea..c24a496486c 100644
--- a/app/views/projects/_bitbucket_import_modal.html.haml
+++ b/app/views/projects/_bitbucket_import_modal.html.haml
@@ -1,8 +1,8 @@
-%div#bitbucket_import_modal.modal
+#bitbucket_import_modal.modal
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3 Import projects from Bitbucket
.modal-body
To enable importing projects from Bitbucket,
@@ -10,4 +10,4 @@
as administrator you need to configure
- else
ask your GitLab administrator to configure
- == #{link_to 'OAuth integration', help_page_path("integration/bitbucket")}.
+ = link_to 'OAuth integration', help_page_path("integration/bitbucket")
diff --git a/app/views/projects/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml
index d2c1e943db1..e2b73cee5a9 100644
--- a/app/views/projects/_customize_workflow.html.haml
+++ b/app/views/projects/_customize_workflow.html.haml
@@ -1,5 +1,5 @@
.row-content-block.project-home-empty
- %div.text-center{ class: container_class }
+ .text-center{ class: container_class }
%h4
Customize your workflow!
%p
diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml
index 08e2fc48be7..dbb33090670 100644
--- a/app/views/projects/_find_file_link.html.haml
+++ b/app/views/projects/_find_file_link.html.haml
@@ -1,3 +1,3 @@
-= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do
- = icon('search')
- %span Find File
+= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do
+ = icon('search')
+ %span Find File
diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml
index e9f39b16aa7..00aef66e1f8 100644
--- a/app/views/projects/_gitlab_import_modal.html.haml
+++ b/app/views/projects/_gitlab_import_modal.html.haml
@@ -1,8 +1,8 @@
-%div#gitlab_import_modal.modal
+#gitlab_import_modal.modal
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3 Import projects from GitLab.com
.modal-body
To enable importing projects from GitLab.com,
@@ -10,4 +10,4 @@
as administrator you need to configure
- else
ask your GitLab administrator to configure
- == #{link_to 'OAuth integration', help_page_path("integration/gitlab")}.
+ = link_to 'OAuth integration', help_page_path("integration/gitlab")
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 5a04c3318cf..1b9d87e9969 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -5,7 +5,7 @@
= project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile')
%h1.project-title
= @project.name
- %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, fw: false)
.project-home-desc
@@ -18,11 +18,19 @@
= link_to project_path(forked_from_project) do
= forked_from_project.namespace.try(:name)
- .project-repo-buttons.project-action-buttons
+ .project-repo-buttons
.count-buttons
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- - if @project.feature_available?(:repository, current_user)
- .project-clone-holder
- = render "shared/clone_panel"
+ %span.hidden-xs
+ - if @project.feature_available?(:repository, current_user)
+ .project-clone-holder
+ = render "shared/clone_panel"
+
+ - if current_user && can?(current_user, :download_code, @project)
+ = render 'projects/buttons/download', project: @project, ref: @ref
+ = render 'projects/buttons/dropdown'
+ = render 'shared/notifications/button', notification_setting: @notification_setting
+ = render 'projects/buttons/koding'
+ = render 'shared/members/access_request_buttons', source: @project
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index 7f530708947..e1fea8ccf3d 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -1,7 +1,8 @@
+
- ref = local_assigns.fetch(:ref)
- status = commit.status(ref)
- if status
- = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
+ = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status)
= ci_label_for_status(status)
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 58d961d93ca..085f79de785 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -15,23 +15,23 @@
%li.pull-right
.toolbar-group
- = markdown_toolbar_button({icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
- = markdown_toolbar_button({icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" })
- = markdown_toolbar_button({icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
- = markdown_toolbar_button({icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
- = markdown_toolbar_button({icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
- = markdown_toolbar_button({icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
- = markdown_toolbar_button({icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
+ = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
+ = markdown_toolbar_button({ icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" })
+ = markdown_toolbar_button({ icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
+ = markdown_toolbar_button({ icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
+ = markdown_toolbar_button({ icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
+ = markdown_toolbar_button({ icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
+ = markdown_toolbar_button({ icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
.toolbar-group
%button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
- =icon("arrows-alt fw")
+ = icon("arrows-alt fw")
.md-write-holder
= yield
- .md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))}
+ .md.md-preview-holder.js-md-preview.hide{ class: (preview_class if defined?(preview_class)) }
- if defined?(referenced_users) && referenced_users
- %div.referenced-users.hide
+ .referenced-users.hide
%span
= icon("exclamation-triangle")
You are about to add
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
new file mode 100644
index 00000000000..afe2fd7fd7b
--- /dev/null
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -0,0 +1,15 @@
+- form = local_assigns.fetch(:form)
+
+.form-group
+ .checkbox.builds-feature
+ = form.label :only_allow_merge_if_build_succeeds do
+ = form.check_box :only_allow_merge_if_build_succeeds
+ %strong Only allow merge requests to be merged if the build succeeds
+ %br
+ %span.descr
+ Builds need to be configured to enable this feature.
+ = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
+ .checkbox
+ = form.label :only_allow_merge_if_all_discussions_are_resolved do
+ = form.check_box :only_allow_merge_if_all_discussions_are_resolved
+ %strong Only allow merge requests to be merged if all discussions are resolved
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index 6e143c4b570..818010bc7d3 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -1,18 +1,8 @@
-.merge-requests-feature
- %fieldset.builds-feature
- %hr
- %h5.prepend-top-0
- Merge Requests
- .form-group
- .checkbox
- = f.label :only_allow_merge_if_build_succeeds do
- = f.check_box :only_allow_merge_if_build_succeeds
- %strong Only allow merge requests to be merged if the build succeeds
- %br
- %span.descr
- Builds need to be configured to enable this feature.
- = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
- .checkbox
- = f.label :only_allow_merge_if_all_discussions_are_resolved do
- = f.check_box :only_allow_merge_if_all_discussions_are_resolved
- %strong Only allow merge requests to be merged if all discussions are resolved
+- form = local_assigns.fetch(:form)
+
+%fieldset.features.merge-requests-feature.append-bottom-default
+ %hr
+ %h5.prepend-top-0
+ Merge Requests
+
+ = render 'projects/merge_request_merge_settings', form: form
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index 369a847e7d4..b6fb08b68e9 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -18,5 +18,5 @@
distributed with computer software, forming part of its documentation.
%p
We recommend you to
- = link_to "add a README", new_readme_path, class: 'underlined-link'
+ = link_to "add a README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link'
file to the repository and GitLab will render it here instead of this message.
diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml
new file mode 100644
index 00000000000..65fc0a36ca9
--- /dev/null
+++ b/app/views/projects/_visibility_select.html.haml
@@ -0,0 +1,7 @@
+- if can_change_visibility_level?(@project, current_user)
+ = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select')
+- else
+ .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } }
+ = visibility_level_icon(@project.visibility_level)
+ %strong
+ = visibility_level_label(@project.visibility_level)
diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml
index f00422dd7c0..41d42740f61 100644
--- a/app/views/projects/_wiki.html.haml
+++ b/app/views/projects/_wiki.html.haml
@@ -7,7 +7,7 @@
- else
- can_create_wiki = can?(current_user, :create_wiki, @project)
.project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
- %div.text-center{ class: container_class }
+ .text-center{ class: container_class }
%h4
This project does not have a wiki homepage yet
- if can_create_wiki
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
index def493c56f5..9e49c93388a 100644
--- a/app/views/projects/artifacts/_tree_directory.html.haml
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -1,6 +1,6 @@
- path_to_directory = browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path)
-%tr.tree-item{ 'data-link' => path_to_directory}
+%tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name
= tree_icon('folder', '755', directory.name)
%span.str-truncated
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 539d07d634a..d0ff14e45e6 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,5 +1,4 @@
- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
-- header_title project_title(@project, "Builds", project_builds_path(@project))
.top-block.row-content-block.clearfix
.pull-right
@@ -9,7 +8,7 @@
Download artifacts archive
.tree-holder
- %div.tree-content-holder
+ .tree-content-holder
%table.table.tree-table
%thead
%tr
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index cadfe5a3e30..23f54553014 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -31,12 +31,12 @@
&nbsp;
.light
= commit_author_link(commit, avatar: false)
- authored
+ committed
#{time_ago_with_tooltip(commit.committed_date)}
%td.line-numbers
- line_count = blame_group[:lines].count
- (current_line...(current_line + line_count)).each do |i|
- %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ %a.diff-line-num{ href: "#L#{i}", id: "L#{i}", 'data-line-number' => i }
= icon("link")
= i
\
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 149ee7c59d6..f75f438ee4f 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -18,11 +18,11 @@
- else
= link_to title, '#'
-%ul.blob-commit-info.hidden-xs
+%ul.blob-commit-info.table-list.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project, ref: @ref
-%div#blob-content-holder.blob-content-holder
+#blob-content-holder.blob-content-holder
%article.file-holder
.file-title
= blob_icon blob.mode, blob.name
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 4a6aa92e3f3..1d058daa094 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -21,6 +21,8 @@
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
+ .dockerfile-selector.js-dockerfile-selector-wrap.hidden
+ = dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
= button_tag class: 'soft-wrap-toggle btn', type: 'button' do
%span.no-wrap
= custom_icon('icon_no_wrap')
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index 4c356d1f07f..a486b2fe491 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -5,11 +5,11 @@
- # be wrong/strange if RawController modified the data.
- blob.load_all_data!(@repository)
- blob = sanitize_svg(blob)
- %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
+ %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: "#{blob.name}" }
- else
.nothing-here-block
The SVG could not be displayed as it is too large, you can
#{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')}
instead.
- else
- %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))}
+ %img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" }
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index 84694203d7d..7f470b890ba 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title Create New Directory
.modal-body
= form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index 2e1f32fd15e..db6662a95ac 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title Delete #{@blob.name}
.modal-body
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 57a27ec904e..61a7ffdd0ab 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title #{title}
.modal-body
= form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do
@@ -12,7 +12,7 @@
Attach a file by drag &amp; drop or
= link_to 'click to upload', '#', class: "markdown-selector"
%br
- .dropzone-alerts{class: "alert alert-danger data", style: "display:none"}
+ .dropzone-alerts.alert.alert-danger.data{ style: "display:none" }
= render 'shared/new_commit_form', placeholder: placeholder
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index a79ae53c780..538f8591f13 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -13,15 +13,15 @@
- case diff_view
- when :inline
%td.old_line.diff-line-num{ data: { linenumber: line_old } }
- %a{href: "##{line_old}", data: { linenumber: line_old }}
+ %a{ href: "##{line_old}", data: { linenumber: line_old } }
%td.new_line.diff-line-num{ data: { linenumber: line_new } }
- %a{href: "##{line_new}", data: { linenumber: line_new }}
+ %a{ href: "##{line_new}", data: { linenumber: line_new } }
= line_content
- when :parallel
- %td.old_line.diff-line-num{data: { linenumber: line_old }}
+ %td.old_line.diff-line-num{ data: { linenumber: line_old } }
= link_to raw(line_old), "##{line_old}"
= line_content
- %td.new_line.diff-line-num{data: { linenumber: line_new }}
+ %td.new_line.diff-line-num{ data: { linenumber: line_new } }
= link_to raw(line_new), "##{line_new}"
= line_content
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 2a0352a71b7..a5dcd93f42e 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -27,5 +27,5 @@
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha
= hidden_field_tag 'content', '', id: "file-content"
- = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
+ = hidden_field_tag 'from_merge_request_iid', params[:from_merge_request_iid]
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml
index 541dc96c45f..5cafb644b40 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -20,6 +20,6 @@
- else
%td.old_line.diff-line-num
%td.new_line.diff-line-num
- %td.line_content{class: "#{line.type}"}= diff_line_content(line.text)
+ %td.line_content{ class: "#{line.type}" }= diff_line_content(line.text)
- else
.nothing-here-block No changes.
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 0ab78a39cf9..b6738c3380f 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -5,7 +5,7 @@
%div{ class: container_class }
= render 'projects/last_push'
- %div#tree-holder.tree-holder
+ #tree-holder.tree-holder
= render 'blob', blob: @blob
- if can_edit_blob?(@blob)
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
new file mode 100644
index 00000000000..356bd50f7f3
--- /dev/null
+++ b/app/views/projects/boards/_show.html.haml
@@ -0,0 +1,28 @@
+- @no_container = true
+- @content_class = "issue-boards-content"
+- page_title "Boards"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('boards/boards_bundle.js')
+ = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
+
+ %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
+ %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
+ %script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card"
+
+= render "projects/issues/head"
+
+= render 'shared/issuable/filter', type: :boards
+
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+ .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ %board{ "v-cloak" => true,
+ "v-for" => "list in state.lists",
+ "ref" => "board",
+ ":list" => "list",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase",
+ ":key" => "_uid" }
+ = render "projects/boards/components/sidebar"
diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml
index 97eb952eff1..0af40ddf8fe 100644
--- a/app/views/projects/boards/components/_blank_state.html.haml
+++ b/app/views/projects/boards/components/_blank_state.html.haml
@@ -1,5 +1,5 @@
%board-blank-state{ "inline-template" => true,
- "v-if" => "list.id == 'blank'" }
+ "v-if" => 'list.id == "blank"' }
.board-blank-state
%p
Add the following default lists to your Issue Board with one click:
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index f7071051efc..a2e5118a9f3 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -1,80 +1,34 @@
-%board{ "inline-template" => true,
- "v-cloak" => true,
- "v-for" => "list in state.lists | orderBy 'position'",
- "v-ref:board" => true,
- ":list" => "list",
- ":disabled" => "disabled",
- ":issue-link-base" => "issueLinkBase",
- "track-by" => "_uid" }
- .board{ ":class" => "{ 'is-draggable': !list.preset }",
- ":data-id" => "list.id" }
- .board-inner
- %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
- %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
- %span.has-tooltip{ ":title" => "(list.label ? list.label.description : '')",
- data: { container: "body", placement: "bottom" } }
- {{ list.title }}
- .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
- %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
- {{ list.issuesSize }}
- - if can?(current_user, :admin_issue, @project)
- %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
- "@click" => "showNewIssueForm",
- "v-if" => "list.type !== 'done'",
- "aria-label" => "Add an issue",
- "title" => "Add an issue",
- data: { placement: "top", container: "body" } }
- = icon("plus")
- - if can?(current_user, :admin_list, @project)
- %board-delete{ "inline-template" => true,
- ":list" => "list",
- "v-if" => "!list.preset && list.id" }
- %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
- = icon("trash")
- %board-list{ "inline-template" => true,
- "v-if" => "list.type !== 'blank'",
- ":list" => "list",
- ":issues" => "list.issues",
- ":loading" => "list.loading",
- ":disabled" => "disabled",
- ":show-issue-form.sync" => "showIssueForm",
- ":issue-link-base" => "issueLinkBase" }
- .board-list-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- - if can? current_user, :create_issue, @project
- %board-new-issue{ "inline-template" => true,
+.board{ ":class" => '{ "is-draggable": !list.preset }',
+ ":data-id" => "list.id" }
+ .board-inner
+ %header.board-header{ ":class" => '{ "has-border": list.label }', ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
+ %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
+ %span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")',
+ data: { container: "body", placement: "bottom" } }
+ {{ list.title }}
+ .board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
+ %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' }
+ {{ list.issuesSize }}
+ - if can?(current_user, :admin_issue, @project)
+ %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
+ "@click" => "showNewIssueForm",
+ "v-if" => 'list.type !== "done"',
+ "aria-label" => "Add an issue",
+ "title" => "Add an issue",
+ data: { placement: "top", container: "body" } }
+ = icon("plus")
+ - if can?(current_user, :admin_list, @project)
+ %board-delete{ "inline-template" => true,
":list" => "list",
- ":show-issue-form.sync" => "showIssueForm",
- "v-show" => "list.type !== 'done' && showIssueForm" }
- .card.board-new-issue-form
- %form{ "@submit" => "submit($event)" }
- .flash-container{ "v-if" => "error" }
- .flash-alert
- An error occured. Please try again.
- %label.label-light{ ":for" => "list.id + '-title'" }
- Title
- %input.form-control{ type: "text",
- "v-model" => "title",
- "v-el:input" => true,
- ":id" => "list.id + '-title'" }
- .clearfix.prepend-top-10
- %button.btn.btn-success.pull-left{ type: "submit",
- ":disabled" => "title === ''",
- "v-el:submit-button" => true }
- Submit issue
- %button.btn.btn-default.pull-right{ type: "button",
- "@click" => "cancel" }
- Cancel
- %ul.board-list{ "v-el:list" => true,
- "v-show" => "!loading",
- ":data-board" => "list.id",
- ":class" => "{ 'is-smaller': showIssueForm }" }
- = render "projects/boards/components/card"
- %li.board-list-count.text-center{ "v-if" => "showCount" }
- = icon("spinner spin", "v-show" => "list.loadingMore" )
- %span{ "v-if" => "list.issues.length === list.issuesSize" }
- Showing all issues
- %span{ "v-else" => true }
- Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
- - if can?(current_user, :admin_list, @project)
- = render "projects/boards/components/blank_state"
+ "v-if" => "!list.preset && list.id" }
+ %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+ = icon("trash")
+ %board-list{ "v-if" => 'list.type !== "blank"',
+ ":list" => "list",
+ ":issues" => "list.issues",
+ ":loading" => "list.loading",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase",
+ "ref" => "board-list" }
+ - if can?(current_user, :admin_list, @project)
+ = render "projects/boards/components/blank_state"
diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml
new file mode 100644
index 00000000000..34fdb1f6a74
--- /dev/null
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -0,0 +1,44 @@
+.board-list-component
+ .board-list-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ - if can? current_user, :create_issue, @project
+ %board-new-issue{ "inline-template" => true,
+ ":list" => "list",
+ "v-if" => 'list.type !== "done" && showIssueForm' }
+ .card.board-new-issue-form
+ %form{ "@submit" => "submit($event)" }
+ .flash-container{ "v-if" => "error" }
+ .flash-alert
+ An error occured. Please try again.
+ %label.label-light{ ":for" => 'list.id + "-title"' }
+ Title
+ %input.form-control{ type: "text",
+ "v-model" => "title",
+ "ref" => "input",
+ ":id" => 'list.id + "-title"' }
+ .clearfix.prepend-top-10
+ %button.btn.btn-success.pull-left{ type: "submit",
+ ":disabled" => 'title === ""',
+ "ref" => "submit-button" }
+ Submit issue
+ %button.btn.btn-default.pull-right{ type: "button",
+ "@click" => "cancel" }
+ Cancel
+ %ul.board-list{ "ref" => "list",
+ "v-show" => "!loading",
+ ":data-board" => "list.id",
+ ":class" => '{ "is-smaller": showIssueForm }' }
+ %board-card{ "v-for" => "(issue, index) in orderedIssues",
+ "ref" => "issue",
+ ":index" => "index",
+ ":list" => "list",
+ ":issue" => "issue",
+ ":issue-link-base" => "issueLinkBase",
+ ":disabled" => "disabled",
+ ":key" => "issue.id" }
+ %li.board-list-count.text-center{ "v-if" => "showCount" }
+ = icon("spinner spin", "v-show" => "list.loadingMore" )
+ %span{ "v-if" => "list.issues.length === list.issuesSize" }
+ Showing all issues
+ %span{ "v-else" => true }
+ Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index 8fce702314c..e4c2aff46ec 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -1,36 +1,28 @@
-%board-card{ "inline-template" => true,
- "v-for" => "issue in issues | orderBy 'priority'",
- "v-ref:issue" => true,
- ":index" => "$index",
- ":list" => "list",
- ":issue" => "issue",
- ":issue-link-base" => "issueLinkBase",
- ":disabled" => "disabled",
- "track-by" => "id" }
- %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
- ":index" => "index",
- "@mousedown" => "mouseDown",
- "@mouseMove" => "mouseMove",
- "@mouseup" => "showIssue($event)" }
- %h4.card-title
- = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
- %a{ ":href" => "issueLinkBase + '/' + issue.id",
- ":title" => "issue.title" }
- {{ issue.title }}
- .card-footer
- %span.card-number{ "v-if" => "issue.id" }
- = precede '#' do
- {{ issue.id }}
- %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
- ":title" => "'Assigned to ' + issue.assignee.name",
- "v-if" => "issue.assignee",
- data: { container: 'body' } }
- %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
- %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
- type: "button",
- "v-if" => "(!list.label || label.id !== list.label.id)",
- "@click" => "filterByLabel(label, $event)",
- ":style" => "{ backgroundColor: label.color, color: label.textColor }",
- ":title" => "label.description",
- data: { container: 'body' } }
- {{ label.title }}
+%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }',
+ ":index" => "index",
+ ":data-issue-id" => "issue.id",
+ "@mousedown" => "mouseDown",
+ "@mousemove" => "mouseMove",
+ "@mouseup" => "showIssue($event)" }
+ %h4.card-title
+ = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
+ %a{ ":href" => 'issueLinkBase + "/" + issue.id',
+ ":title" => "issue.title" }
+ {{ issue.title }}
+ .card-footer
+ %span.card-number{ "v-if" => "issue.id" }
+ = precede '#' do
+ {{ issue.id }}
+ %a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username",
+ ":title" => '"Assigned to " + issue.assignee.name',
+ "v-if" => "issue.assignee",
+ data: { container: 'body' } }
+ %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20, alt: "Avatar" }
+ %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
+ type: "button",
+ "v-if" => "(!list.label || label.id !== list.label.id)",
+ "@click" => "filterByLabel(label, $event)",
+ ":style" => "{ backgroundColor: label.color, color: label.textColor }",
+ ":title" => "label.description",
+ data: { container: 'body' } }
+ {{ label.title }}
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
index f0c0c6953e0..df7fa9ddaf2 100644
--- a/app/views/projects/boards/components/_sidebar.html.haml
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -1,23 +1,24 @@
%board-sidebar{ "inline-template" => true,
- ":current-user" => "#{current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) if current_user}" }
- %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
- .issuable-sidebar
- .block.issuable-sidebar-header
- %span.issuable-header-text.hide-collapsed.pull-left
- %strong
- {{ issue.title }}
- %br/
- %span
- = precede "#" do
- {{ issue.id }}
- %a.gutter-toggle.pull-right{ role: "button",
- href: "#",
- "@click.prevent" => "closeSidebar",
- "aria-label" => "Toggle sidebar" }
- = custom_icon("icon_close", size: 15)
- .js-issuable-update
- = render "projects/boards/components/sidebar/assignee"
- = render "projects/boards/components/sidebar/milestone"
- = render "projects/boards/components/sidebar/due_date"
- = render "projects/boards/components/sidebar/labels"
- = render "projects/boards/components/sidebar/notifications"
+ ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" }
+ %transition{ name: "boards-sidebar-slide" }
+ %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
+ .issuable-sidebar
+ .block.issuable-sidebar-header
+ %span.issuable-header-text.hide-collapsed.pull-left
+ %strong
+ {{ issue.title }}
+ %br/
+ %span
+ = precede "#" do
+ {{ issue.id }}
+ %a.gutter-toggle.pull-right{ role: "button",
+ href: "#",
+ "@click.prevent" => "closeSidebar",
+ "aria-label" => "Toggle sidebar" }
+ = custom_icon("icon_close", size: 15)
+ .js-issuable-update
+ = render "projects/boards/components/sidebar/assignee"
+ = render "projects/boards/components/sidebar/milestone"
+ = render "projects/boards/components/sidebar/due_date"
+ = render "projects/boards/components/sidebar/labels"
+ = render "projects/boards/components/sidebar/notifications"
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
index 604e13858d1..e75ce305440 100644
--- a/app/views/projects/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -1,8 +1,8 @@
.block.assignee
.title.hide-collapsed
Assignee
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value.hide-collapsed
%span.assign-yourself.no-value{ "v-if" => "!issue.assignee" }
@@ -14,7 +14,7 @@
%a.author_link.bold{ ":href" => "'#{root_url}' + issue.assignee.username",
"v-if" => "issue.assignee" }
%img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar",
- width: "32" }
+ width: "32", alt: "Avatar" }
%span.author
{{ issue.assignee.name }}
%span.username
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
index c7da1d0d4ac..1a3b88e28c5 100644
--- a/app/views/projects/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml
@@ -1,8 +1,8 @@
.block.due_date
.title
Due date
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value
.value-content
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
index ce68e5e1998..0f0a84c156d 100644
--- a/app/views/projects/boards/components/sidebar/_labels.html.haml
+++ b/app/views/projects/boards/components/sidebar/_labels.html.haml
@@ -1,8 +1,8 @@
.block.labels
.title
Labels
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value.issuable-show-labels
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
index 3cd20d1c0f7..008d1186478 100644
--- a/app/views/projects/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -1,8 +1,8 @@
.block.milestone
.title
Milestone
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value
%span.no-value{ "v-if" => "!issue.milestone" }
diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml
index 21c9563e9db..a08c7f2af09 100644
--- a/app/views/projects/boards/components/sidebar/_notifications.html.haml
+++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml
@@ -1,11 +1,7 @@
- if current_user
.block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" }
- .title
+ %span.issuable-header-text.hide-collapsed.pull-left
Notifications
- %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }}
- .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" }
- .unsubscribed{ "v-show" => "!issue.subscribed" }
- You're not receiving notifications from this thread.
- .subscribed{ "v-show" => "issue.subscribed" }
- You're receiving notifications because you're subscribed to this thread.
+ %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ %span
+ {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml
index 29c9a43a0c1..2a5b8b1441e 100644
--- a/app/views/projects/boards/index.html.haml
+++ b/app/views/projects/boards/index.html.haml
@@ -1,18 +1 @@
-- @no_container = true
-- @content_class = "issue-boards-content"
-- page_title "Boards"
-
-- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('boards/boards_bundle.js')
- = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
-
-= render "projects/issues/head"
-
-= render 'shared/issuable/filter', type: :boards
-
-#board-app.boards-app{ "v-cloak" => true, data: board_data }
- .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
- = render "projects/boards/components/sidebar"
+= render "show"
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
index 29c9a43a0c1..2a5b8b1441e 100644
--- a/app/views/projects/boards/show.html.haml
+++ b/app/views/projects/boards/show.html.haml
@@ -1,18 +1 @@
-- @no_container = true
-- @content_class = "issue-boards-content"
-- page_title "Boards"
-
-- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('boards/boards_bundle.js')
- = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
-
-= render "projects/issues/head"
-
-= render 'shared/issuable/filter', type: :boards
-
-#board-app.boards-app{ "v-cloak" => true, data: board_data }
- .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
- = render "projects/boards/components/sidebar"
+= render "show"
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 9135cee8364..04efc2e996c 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -4,7 +4,7 @@
- number_commits_behind = diverging_commit_counts[:behind]
- number_commits_ahead = diverging_commit_counts[:ahead]
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
-%li(class="js-branch-#{branch.name}")
+%li{ class: "js-branch-#{branch.name}" }
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do
= branch.name
@@ -12,12 +12,11 @@
- if branch.name == @repository.root_ref
%span.label.label-primary default
- elsif @repository.merged_to_root_ref? branch.name
- %span.label.label-info.has-tooltip(title="Merged into #{@repository.root_ref}")
+ %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" }
merged
- if @project.protected_branch? branch.name
%span.label.label-success
- %i.fa.fa-lock
protected
.controls.hidden-xs
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 84f38575e84..5f8f56150f9 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -3,19 +3,20 @@
= render "projects/commits/head"
%div{ class: container_class }
- .top-area
+ .top-area.adjust
.nav-text
- Protected branches can be managed in project settings
+ Protected branches can be managed in
+ = link_to 'project settings', namespace_project_protected_branches_path(@project.namespace, @project)
.nav-controls
= form_tag(filter_branches_path, method: :get) do
= search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light
= projects_sort_options_hash[@sort]
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_branches_path(sort: sort_value_name) do
@@ -26,6 +27,8 @@
= sort_title_oldest_updated
- if can? current_user, :push_code, @project
+ = link_to namespace_project_merged_branches_path(@project.namespace, @project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do
+ Delete merged branches
= link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
New branch
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 5a6c8c243fa..e63bdb38bd8 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -2,7 +2,7 @@
- if @error
.alert.alert-danger
- %button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
+ %button.close{ type: "button", "data-dismiss" => "alert" } &times;
= @error
%h3.page-title
New Branch
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 3f2ce7377fd..b15be0d861d 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,10 +1,13 @@
.content-block.build-header
.header-content
- = ci_status_with_icon(@build.status)
+ = render 'ci/status/badge', status: @build.detailed_status(current_user)
Build
%strong ##{@build.id}
+ in pipeline
+ = link_to pipeline_path(@build.pipeline) do
+ %strong ##{@build.pipeline.id}
for commit
- = link_to ci_status_path(@build.pipeline) do
+ = link_to namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha) do
%strong= @build.pipeline.short_sha
from
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
@@ -14,6 +17,6 @@
= render "user"
= time_ago_with_tooltip(@build.created_at)
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', method: :post
+ = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 28f519f11b2..37bf085130a 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -22,14 +22,14 @@
%p.build-detail-row
The artifacts were removed
#{time_ago_with_tooltip(@build.artifacts_expire_at)}
- - elsif @build.artifacts_expire_at
+ - elsif @build.has_expiring_artifacts?
%p.build-detail-row
The artifacts will be removed in
%span.js-artifacts-remove= @build.artifacts_expire_at
- if @build.artifacts?
.btn-group.btn-group-justified{ role: :group }
- - if @build.artifacts_expire_at
+ - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
= link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep
@@ -111,28 +111,29 @@
%span.label.label-primary
= tag
- - if @build.pipeline.stages.many?
+ - if @build.pipeline.stages_count > 1
.dropdown.build-dropdown
.title Stage
- %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.stage-selection More
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu
- @build.pipeline.stages.each do |stage|
%li
- %a.stage-item= stage
+ %a.stage-item= stage.name
.builds-container
- HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build|
- .build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}}
+ .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
= icon('arrow-right')
- = ci_icon_for_status(build.status)
+ %span{ class: "ci-status-icon-#{build.status}" }
+ = ci_icon_for_status(build.status)
%span
- if build.name
= build.name
- else
= build.id
- if build.retried?
- %i.fa.fa-refresh.has-tooltip{data: { container: 'body', placement: 'bottom' }, title: 'Build was retried'}
+ %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Build was retried' }
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
index 36294c89fa8..028664f5bba 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -10,6 +10,7 @@
%tr
%th Status
%th Build
+ %th Pipeline
- if admin
%th Project
%th Runner
@@ -19,6 +20,6 @@
%th Coverage
%th
- = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin }
+ = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, pipeline_link: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin }
= paginate builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 06070f12bbd..c623e39b21f 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -19,5 +19,5 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
- %div.content-list.builds-content-list
+ .content-list.builds-content-list
= render "table", builds: @builds, project: @project
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index ae7a7ecb392..c613e473e4c 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,6 +1,6 @@
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Builds"
-- header_title project_title(@project, "Builds", project_builds_path(@project))
+- trace_with_state = @build.trace_with_state
= render "projects/pipelines/head", build_subnav: true
%div{ class: container_class }
@@ -26,25 +26,54 @@
= link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
Runners page
+ - if @build.starts_environment?
+ .prepend-top-default
+ .environment-information
+ - if @build.outdated_deployment?
+ = ci_icon_for_status('success_with_warnings')
+ - else
+ = ci_icon_for_status(@build.status)
+
+ - environment = environment_for_build(@build.project, @build)
+ - if @build.success? && @build.last_deployment.present?
+ - if @build.last_deployment.last?
+ This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
+ - else
+ This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
+ View the most recent deployment #{deployment_link(environment.last_deployment)}.
+ - elsif @build.complete? && !@build.success?
+ The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed.
+ - else
+ This build is creating a deployment to #{environment_link_for_build(@build.project, @build)}
+ - if environment.try(:last_deployment)
+ and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
+
.prepend-top-default
- if @build.erased?
.erased.alert.alert-warning
- - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
- Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
+ - if @build.erased_by_user?
+ Build has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
+ - else
+ Build has been erased #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
.scroll-step
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
+ %a.scroll-link.scroll-top{ href: '#up-build-trace', id: 'scroll-top', title: 'Scroll to top' }
+ = custom_icon('scroll_up')
+ = custom_icon('scroll_up_hover_active')
+ %a.scroll-link.scroll-bottom{ href: '#down-build-trace', id: 'scroll-bottom', title: 'Scroll to bottom' }
+ = custom_icon('scroll_down')
+ = custom_icon('scroll_down_hover_active')
- if @build.active?
.autoscroll-container
- %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
- Enable autoscroll
+ %span.status-message#autoscroll-status{ data: { state: 'disabled' } }
+ %span.status-text Autoscroll active
+ %i.status-icon
+ = custom_icon('scroll_down_hover_active')
+ #up-build-trace
%pre.build-trace#build-trace
%code.bash.js-build-output
- = icon("refresh spin", class: "js-build-refresh")
+ .build-loader-animation.js-build-refresh
#down-build-trace
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 7e83a88913a..324a7f8cd3f 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,42 +1,41 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- %span{class: 'hidden-xs hidden-sm download-button'}
- .dropdown.inline
- %button.btn{ 'data-toggle' => 'dropdown' }
- = icon('download')
- = icon("caret-down")
- %span.sr-only
- Select Archive Format
- %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
- %li.dropdown-header Source code
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download zip
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar.gz
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar.bz2
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar
+ .project-action-button.dropdown.inline
+ %button.btn{ 'data-toggle' => 'dropdown' }
+ = icon('download')
+ = icon("caret-down")
+ %span.sr-only
+ Select Archive Format
+ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
+ %li.dropdown-header Source code
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download zip
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar.gz
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar.bz2
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar
- - pipeline = project.pipelines.latest_successful_for(ref)
- - if pipeline
- - artifacts = pipeline.builds.latest.with_artifacts
- - if artifacts.any?
- %li.dropdown-header Artifacts
- - unless pipeline.latest?
- - latest_pipeline = project.pipeline_for(ref)
- %li
- .unclickable= ci_status_for_statuseable(latest_pipeline)
- %li.dropdown-header Previous Artifacts
- - artifacts.each do |job|
- %li
- = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download '#{job.name}'
+ - pipeline = project.pipelines.latest_successful_for(ref)
+ - if pipeline
+ - artifacts = pipeline.builds.latest.with_artifacts
+ - if artifacts.any?
+ %li.dropdown-header Artifacts
+ - unless pipeline.latest?
+ - latest_pipeline = project.pipeline_for(ref)
+ %li
+ .unclickable= ci_status_for_statuseable(latest_pipeline)
+ %li.dropdown-header Previous Artifacts
+ - artifacts.each do |job|
+ %li
+ = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download '#{job.name}'
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 6cd9b98a706..67de8699b2e 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,6 +1,6 @@
- if current_user
- .dropdown.inline.project-dropdown
- %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
+ .project-action-button.dropdown.inline
+ %a.btn.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
= icon('plus')
= icon("caret-down")
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 27da86b9efe..851fe44a86d 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -8,7 +8,7 @@
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do
= custom_icon('icon_fork')
%span Fork
- %div.count-with-arrow
+ .count-with-arrow
%span.arrow
= link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do
= @project.forks_count
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
index fdc80d44253..5d9a776da89 100644
--- a/app/views/projects/buttons/_koding.html.haml
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -1,7 +1,3 @@
-- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch)
- - if @repository.koding_yml
- = link_to koding_project_url(@project), class: 'btn', target: '_blank' do
- Run in IDE (Koding)
- - else
- = link_to add_koding_stack_path(@project), class: 'btn' do
- Set Up Koding
+- if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch)
+ = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do
+ Run in IDE (Koding)
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 12d35101770..d57eb2cbfbc 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -6,7 +6,7 @@
- else
= icon('star-o')
%span Star
- %div.count-with-arrow
+ .count-with-arrow
%span.arrow
%span.count.star-count
= @project.star_count
@@ -15,7 +15,7 @@
= link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do
= icon('star')
Star
- %div.count-with-arrow
+ .count-with-arrow
%span.arrow
%span.count
= @project.star_count
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 94632056b15..520113639b7 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -2,16 +2,14 @@
- ref = local_assigns.fetch(:ref, nil)
- commit_sha = local_assigns.fetch(:commit_sha, nil)
- retried = local_assigns.fetch(:retried, false)
+- pipeline_link = local_assigns.fetch(:pipeline_link, false)
- stage = local_assigns.fetch(:stage, false)
- coverage = local_assigns.fetch(:coverage, false)
- allow_retry = local_assigns.fetch(:allow_retry, false)
-%tr.build.commit{class: ('retried' if retried)}
+%tr.build.commit{ class: ('retried' if retried) }
%td.status
- - if can?(current_user, :read_build, build)
- = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
- - else
- = ci_status_with_icon(build.status)
+ = render "ci/status/badge", status: build.detailed_status(current_user)
%td.branch-commit
- if can?(current_user, :read_build, build)
@@ -51,12 +49,20 @@
- if build.manual?
%span.label.label-info manual
+ - if pipeline_link
+ %td
+ = link_to pipeline_path(build.pipeline) do
+ %span.pipeline-id ##{build.pipeline.id}
+ %span by
+ - if build.pipeline.user
+ = user_avatar(user: build.pipeline.user, size: 20)
+ - else
+ %span.monospace API
+
- if admin
%td
- if build.project
= link_to build.project.name_with_namespace, admin_namespace_project_path(build.project.namespace, build.project)
-
- - if admin
%td
- if build.try(:runner)
= runner_link(build.runner)
@@ -82,9 +88,8 @@
%span #{time_ago_with_tooltip(build.finished_at)}
%td.coverage
- - if coverage
- - if build.try(:coverage)
- #{build.coverage}%
+ - if coverage && build.try(:coverage)
+ #{build.coverage}%
%td
.pull-right
@@ -96,9 +101,9 @@
= link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred')
- elsif allow_retry
- - if build.retryable?
- = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
- = icon('repeat')
- - elsif build.playable? && !admin
+ - if build.playable? && !admin
= link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= custom_icon('icon_play')
+ - elsif build.retryable?
+ = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
+ = icon('repeat')
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
deleted file mode 100644
index 93dca81e6f9..00000000000
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- is_playable = subject.playable? && can?(current_user, :update_build, @project)
-- if is_playable
- = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do
- = ci_icon_for_status('play')
- .ci-status-text= subject.name
-- elsif can?(current_user, :read_build, @project)
- = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do
- %span.ci-status-icon
- = ci_icon_for_status(subject.status)
- .ci-status-text= subject.name
-- else
- %span.ci-status-icon
- = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 2a2d24be736..990bfbcf951 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -4,8 +4,7 @@
%tr.commit
%td.commit-link
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
- = ci_status_with_icon(status)
+ = render 'ci/status/badge', status: pipeline.detailed_status(current_user)
%td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
@@ -41,15 +40,25 @@
- else
Cant find HEAD commit for this branch
- - stages_status = pipeline.statuses.latest.stages_status
%td.stage-cell
- - stages.each do |stage|
- - status = stages_status[stage]
- - tooltip = "#{stage.titleize}: #{status || 'not found'}"
- - if status
- .stage-container
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
- = ci_icon_for_status(status)
+ - pipeline.stages.each do |stage|
+ - if stage.status
+ - detailed_status = stage.detailed_status(current_user)
+ - icon_status = "#{detailed_status.icon}_borderless"
+ - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
+
+ .stage-container.dropdown.js-mini-pipeline-graph
+ %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
+ = custom_icon(icon_status)
+ = icon('caret-down')
+
+ %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
+ .arrow-up
+ .js-builds-dropdown-list.scrollable-menu
+
+ .js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %span.fa.fa-spinner.fa-spin
+
%td
- if pipeline.duration
@@ -69,18 +78,18 @@
.btn-group.inline
- if actions.any?
.btn-group
- %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' }
= custom_icon('icon_play')
- = icon('caret-down')
+ = icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
= link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
- %span= build.name.humanize
+ %span= build.name
- if artifacts.present?
.btn-group
- %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
deleted file mode 100644
index b7087749428..00000000000
--- a/app/views/projects/commit/_builds.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- @ci_pipelines.each do |pipeline|
- = render "pipeline", pipeline: pipeline, pipeline_details: true
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index e4cd55b9f7a..421b3db342d 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -6,29 +6,28 @@
- label = 'Cherry-pick'
- target_label = 'Pick into branch'
-.modal{id: "modal-#{type}-commit"}
+.modal{ id: "modal-#{type}-commit" }
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3.page-title== #{label} this #{commit.change_type_title}
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
+ %h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body
- = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do
+ = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
= hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch'
- = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false }})
+ = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } })
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
- - nonce = SecureRandom.hex
- = label_tag "create_merge_request-#{nonce}" do
- = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
+ = label_tag do
+ = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil
Start a <strong>new merge request</strong> with these changes
- else
- = hidden_field_tag 'create_merge_request', 1
+ = hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
= submit_tag label, class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index cbfd99ca448..8aed88da38b 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -7,8 +7,4 @@
= nav_link(path: 'commit#pipelines') do
= link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Pipelines
- %span.badge= @ci_pipelines.count
- = nav_link(path: 'commit#builds') do
- = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
- Builds
- %span.badge= @statuses.count
+ %span.badge= @commit.pipelines.size
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
deleted file mode 100644
index 6bb900e3fc1..00000000000
--- a/app/views/projects/commit/_ci_stage.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-%tr
- %th{colspan: 10}
- %strong
- %a{name: stage}
- - status = statuses.latest.status
- %span{class: "ci-status-link ci-status-icon-#{status}"}
- = ci_icon_for_status(status)
- - if stage
- &nbsp;
- = stage.titleize
- = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
- = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
- %tr
- %td{colspan: 10}
- &nbsp;
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 0ebc38d16cf..08eb0c57f66 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,13 +1,8 @@
-.commit-info-row.commit-info-row-header
- .commit-meta
- %strong Commit
- %strong.monospace.js-details-short= @commit.short_id
- = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
- %span.text-expander
- \...
- %span.js-details-content.hide
- %strong.monospace.commit-hash-full= @commit.id
- = clipboard_button(clipboard_text: @commit.id)
+.page-content-header
+ .header-main-content
+ %strong
+ = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard")
+ = @commit.short_id
%span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)}
%span by
@@ -19,7 +14,8 @@
%strong
= commit_committer_link(@commit, avatar: true, size: 24)
#{time_ago_with_tooltip(@commit.committed_date)}
- .commit-action-buttons
+
+ .header-action-buttons
- if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped.hidden-xs.append-right-10
= icon('comment')
@@ -55,8 +51,8 @@
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
-.commit-info-widget
- .widget-row.branch-info
+.info-well
+ .well-segment.branch-info
.icon-container.commit-icon
= custom_icon("icon_commit")
%span.cgray= pluralize(@commit.parents.count, "parent")
@@ -66,8 +62,8 @@
%i.fa.fa-spinner.fa-spin
- if @commit.status
- .widget-row.pipeline-info
- .icon-container
+ .well-segment.pipeline-info
+ %div{ class: "icon-container ci-status-icon-#{@commit.status}" }
= ci_icon_for_status(@commit.status)
Pipeline
= link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace"
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index d6916fb7f1a..08d3443b3d0 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,10 +1,6 @@
.pipeline-graph-container
.row-content-block.build-content.middle-block.pipeline-actions
.pull-right
- .btn.btn-grouped.btn-white.toggle-pipeline-btn
- %span.toggle-btn-text Hide
- %span pipeline graph
- %span.caret
- if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
@@ -28,20 +24,8 @@
in
= time_interval_in_words pipeline.duration
- .row-content-block.build-content.middle-block.pipeline-graph.hidden
- .pipeline-visualization
- %ul.stage-column-list
- - stages = pipeline.stages_with_latest_statuses
- - stages.each do |stage, statuses|
- %li.stage-column
- .stage-name
- %a{name: stage}
- - if stage
- = stage.titleize
- .builds-container
- %ul
- = render "projects/commit/pipeline_stage", statuses: statuses
-
+ .row-content-block.build-content.middle-block.js-pipeline-graph.hidden
+ = render "projects/pipelines/graph", pipeline: pipeline
- if pipeline.yaml_errors.present?
.bs-callout.bs-callout-danger
@@ -66,5 +50,4 @@
- if pipeline.project.build_coverage_enabled?
%th Coverage
%th
- - pipeline.statuses.relevant.stages.each do |stage|
- = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
+ = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
deleted file mode 100644
index 289aa5178b1..00000000000
--- a/app/views/projects/commit/_pipeline_stage.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-- status_groups = statuses.group_by(&:group_name)
-- status_groups.each do |group_name, grouped_statuses|
- - if grouped_statuses.one?
- - status = grouped_statuses.first
- - is_playable = status.playable? && can?(current_user, :update_build, @project)
- %li.build{ class: ("playable" if is_playable) }
- .curve
- .build-content
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
- - else
- %li.build
- .curve
- .dropdown.inline.build-content
- = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
deleted file mode 100644
index 18daa2ee693..00000000000
--- a/app/views/projects/commit/_pipeline_status_group.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- group_status = CommitStatus.where(id: subject).status
-%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
- %span.ci-status-icon
- = ci_icon_for_status(group_status)
- %span.ci-status-text
- = name
- %span.badge= subject.size
-.dropdown-menu.grouped-pipeline-dropdown
- .arrow
- %ul
- - subject.each do |status|
- %li
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 2dc91a9b762..1164627fa11 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -1,15 +1,15 @@
-%ul.content-list.pipelines
+%div
- if pipelines.blank?
- %li
+ %div
.nothing-here-block No pipelines to show
- else
- .table-holder
- %table.table.ci-table
- %tbody
- %th Status
- %th Pipeline
- %th Commit
- %th Stages
- %th
- %th
- = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, show_commit: false
+ .table-holder.pipelines
+ %table.table.ci-table.js-pipeline-table
+ %thead
+ %th.pipeline-status Status
+ %th.pipeline-info Pipeline
+ %th.pipeline-commit Commit
+ %th.pipeline-stages Stages
+ %th.pipeline-date
+ %th.pipeline-actions
+ = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
deleted file mode 100644
index 077b2d2725b..00000000000
--- a/app/views/projects/commit/builds.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- @no_container = true
-- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
-= render "projects/commits/head"
-
-%div{ class: container_class }
- = render "commit_box"
-
- = render "ci_menu"
- = render "builds"
diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
index 8233e26e4e7..00e7cdd1729 100644
--- a/app/views/projects/commit/pipelines.html.haml
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -3,4 +3,4 @@
= render "commit_box"
= render "ci_menu"
-= render "pipelines_list", pipelines: @ci_pipelines
+= render "pipelines_list", pipelines: @commit.pipelines.order(id: :desc)
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index b8c64d1f13e..7afd3d80ef5 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -8,7 +8,7 @@
- if @commit.status
= render "ci_menu"
- else
- %div.block-connector
+ .block-connector
= render "projects/diffs/diffs", diffs: @diffs
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 9f80a974d64..002e3d345dc 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -6,37 +6,36 @@
- note_count = notes.user.count
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
-- cache_key.push(commit.status) if commit.status
+- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
- %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
- = author_avatar(commit, size: 36)
+ %li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
- .commit-info-block
- .commit-row-title
- %span.item-title
- = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- %span.commit-row-message.visible-xs-inline
- &middot;
- = commit.short_id
- - if commit.status(ref)
- .visible-xs-inline
- = render_commit_status(commit, ref: ref)
- - if commit.description?
- %a.text-expander.hidden-xs.js-toggle-button ...
+ .table-list-cell.avatar-cell.hidden-xs
+ = author_avatar(commit, size: 36)
- .commit-actions.hidden-xs
- - if commit.status(ref)
- = render_commit_status(commit, ref: ref)
- = clipboard_button(clipboard_text: commit.id)
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
- = link_to_browse_code(project, commit)
+ .table-list-cell.commit-content
+ = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
+ %span.commit-row-message.visible-xs-inline
+ &middot;
+ = commit.short_id
+ - if commit.status(ref)
+ .visible-xs-inline
+ = render_commit_status(commit, ref: ref)
+ - if commit.description?
+ %a.text-expander.hidden-xs.js-toggle-button ...
- if commit.description?
%pre.commit-row-description.js-toggle-content
= preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
-
- .commit-row-info
+ .commiter
= commit_author_link(commit, avatar: false, size: 24)
- authored
+ committed
#{time_ago_with_tooltip(commit.committed_date)}
+
+ .table-list-cell.commit-actions.hidden-xs
+ - if commit.status(ref)
+ = render_commit_status(commit, ref: ref)
+ = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
+ = link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index ce416caa494..64d93e4141c 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -1,7 +1,7 @@
- commits, hidden = limited_commits(@commits)
- commits = Commit.decorate(commits, @project)
-%div.panel.panel-default
+.panel.panel-default
.panel-heading
Commits (#{@commits.count})
- if hidden > 0
@@ -11,4 +11,4 @@
%li.warning-row.unstyled
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
- %ul.content-list= render commits, project: @project, ref: @ref
+ %ul.content-list.table-list= render commits, project: @project, ref: @ref
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 48756c68941..fcc367951ad 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -4,7 +4,7 @@
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
%li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}"
%li.commits-row
- %ul.list-unstyled.commit-list
+ %ul.content-list.commit-list.table-list.table-wide
= render commits, project: project, ref: ref
- if hidden > 0
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 9628cbd1634..e77f23c7fd8 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -28,12 +28,12 @@
= search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
- if current_user && current_user.private_token
.control
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do
+ = link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do
= icon("rss")
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
- %div{id: dom_id(@project)}
+ %div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list
= render 'commits', project: @project, ref: @ref
= spinner
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 7bde20c3286..d76d48187cd 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -2,21 +2,21 @@
.clearfix
- if params[:to] && params[:from]
.compare-switch-container
- = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
+ = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Switch base of comparison'}
.form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
.input-group.inline-input-group
%span.input-group-addon from
= hidden_field_tag :from, params[:from]
- = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
- .dropdown-toggle-text= params[:from] || 'Select branch/tag'
+ = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag'
= render "ref_dropdown"
.compare-ellipsis.inline ...
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-addon to
= hidden_field_tag :to, params[:to]
- = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
- .dropdown-toggle-text= params[:to] || 'Select branch/tag'
+ = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
+ .dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag'
= render "ref_dropdown"
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
diff --git a/app/views/projects/cycle_analytics/_empty_stage.html.haml b/app/views/projects/cycle_analytics/_empty_stage.html.haml
new file mode 100644
index 00000000000..c3f95860e92
--- /dev/null
+++ b/app/views/projects/cycle_analytics/_empty_stage.html.haml
@@ -0,0 +1,7 @@
+.empty-stage-container
+ .empty-stage
+ .icon-no-data
+ = custom_icon ('icon_no_data')
+ %h4 We don't have enough data to show this stage.
+ %p
+ {{currentStage.emptyStageText}}
diff --git a/app/views/projects/cycle_analytics/_no_access.html.haml b/app/views/projects/cycle_analytics/_no_access.html.haml
new file mode 100644
index 00000000000..0ffc79b3181
--- /dev/null
+++ b/app/views/projects/cycle_analytics/_no_access.html.haml
@@ -0,0 +1,7 @@
+.no-access-stage-container
+ .no-access-stage
+ .icon-lock
+ = custom_icon ('icon_lock')
+ %h4 You need permission.
+ %p
+ Want to see the data? Please ask administrator for access.
diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml
new file mode 100644
index 00000000000..c8f0b547f80
--- /dev/null
+++ b/app/views/projects/cycle_analytics/_overview.html.haml
@@ -0,0 +1,15 @@
+.cycle-analytics-overview
+ .container
+ .row
+ .col-md-10.col-md-offset-1
+ .row.overview-details
+ .col-md-6.overview-text
+ %h4 Introducing Cycle Analytics
+ %p
+ Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
+ To set up CA, you must first define a production environment by setting up your CI and then deploy to production.
+ %p
+ %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more
+ .col-md-6.overview-image
+ %span.overview-icon
+ = custom_icon ('icon_cycle_analytics_overview')
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 247d612ba6f..479ce44f378 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,63 +1,90 @@
- @no_container = true
- page_title "Cycle Analytics"
-
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('cycle_analytics/cycle_analytics_bundle.js')
+ = page_specific_javascript_tag("cycle_analytics/cycle_analytics_bundle.js")
= render "projects/pipelines/head"
-#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }}
-
- .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
- = icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()")
- .row
- .col-sm-3.col-xs-12.svg-container
- = custom_icon('icon_cycle_analytics_splash')
- .col-sm-8.col-xs-12.inner-content
- %h4
- Introducing Cycle Analytics
- %p
- Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
-
- = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
+#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
+ - if @cycle_analytics_no_data
+ .bordered-box.landing.content-block{ "v-if" => "!isOverviewDialogDismissed" }
+ = icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()")
+ .row
+ .col-sm-3.col-xs-12.svg-container
+ = custom_icon('icon_cycle_analytics_splash')
+ .col-sm-8.col-xs-12.inner-content
+ %h4
+ Introducing Cycle Analytics
+ %p
+ Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
+ = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
= icon("spinner spin", "v-show" => "isLoading")
-
- .wrapper{"v-show" => "!isLoading && !hasError"}
+ .wrapper{ "v-show" => "!isLoading && !hasError" }
.panel.panel-default
.panel-heading
Pipeline Health
-
.content-block
.container-fluid
.row
- .col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"}
- %h3.header {{item.value}}
- %p.text {{item.title}}
-
+ .col-sm-3.col-xs-12.column{ "v-for" => "item in state.summary" }
+ %h3.header {{ item.value }}
+ %p.text {{ item.title }}
.col-sm-3.col-xs-12.column
.dropdown.inline.js-ca-dropdown
- %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
+ %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" }
%span.dropdown-label Last 30 days
%i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right
%li
- %a{'href' => "#", 'data-value' => '30'}
+ %a{ "href" => "#", "data-value" => "30" }
Last 30 days
%li
- %a{'href' => "#", 'data-value' => '90'}
+ %a{ "href" => "#", "data-value" => "90" }
Last 90 days
-
- .bordered-box
- %ul.content-list
- %li{"v-for" => "item in analytics.stats"}
- .container-fluid
- .row
- .col-xs-8.title-col
- %p.title
- {{item.title}}
- %p.text
- {{item.description}}
- .col-xs-4.value-col
- %span
- {{item.value}}
+ .stage-panel-container
+ .panel.panel-default.stage-panel
+ .panel-heading
+ %nav.col-headers
+ %ul
+ %li.stage-header
+ %span.stage-name
+ Stage
+ %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The phase of the development lifecycle.", "aria-hidden" => "true" }
+ %li.median-header
+ %span.stage-name
+ Median
+ %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.", "aria-hidden" => "true" }
+ %li.event-header
+ %span.stage-name
+ {{ currentStage ? currentStage.legend : 'Related Issues' }}
+ %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The collection of events added to the data gathered for that stage.", "aria-hidden" => "true" }
+ %li.total-time-header
+ %span.stage-name
+ Total Time
+ %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The time taken by each data entry gathered by that stage.", "aria-hidden" => "true" }
+ .stage-panel-body
+ %nav.stage-nav
+ %ul
+ %li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" }
+ .stage-nav-item-cell.stage-name
+ {{ stage.title }}
+ .stage-nav-item-cell.stage-median
+ %template{ "v-if" => "stage.isUserAllowed" }
+ %span{ "v-if" => "stage.value" }
+ {{ stage.value }}
+ %span.stage-empty{ "v-else" => true }
+ Not enough data
+ %template{ "v-else" => true }
+ %span.not-available
+ Not available
+ .section.stage-events
+ %template{ "v-if" => "isLoadingStage" }
+ = icon("spinner spin")
+ %template{ "v-if" => "currentStage && !currentStage.isUserAllowed" }
+ = render partial: "no_access"
+ %template{ "v-else" => true }
+ %template{ "v-if" => "isEmptyStage && !isLoadingStage" }
+ = render partial: "empty_stage"
+ %template{ "v-if" => "state.events.length && !isLoadingStage && !isEmptyStage" }
+ %component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.events" }
diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml
index 450aaeb367c..d1e3cb14022 100644
--- a/app/views/projects/deploy_keys/_deploy_key.html.haml
+++ b/app/views/projects/deploy_keys/_deploy_key.html.haml
@@ -6,6 +6,9 @@
= deploy_key.title
.description
= deploy_key.fingerprint
+ - if deploy_key.can_push?
+ .write-access-allowed
+ Write access allowed
.deploy-key-content.prepend-left-default.deploy-key-projects
- deploy_key.projects.each do |project|
- if can?(current_user, :read_project, project)
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index 901605f7ca3..c91bb9c255a 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -10,4 +10,13 @@
%p.light.append-bottom-0
Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh/README")
+ .form-group
+ .checkbox
+ = f.label :can_push do
+ = f.check_box :can_push
+ %strong Write access allowed
+ .form-group
+ %p.light.append-bottom-0
+ Allow this key to push to repository as well? (Default only allows pull access.)
+
= f.submit "Add key", class: "btn-create btn"
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index 58a214bdbd1..a680b1ca017 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -3,7 +3,7 @@
- if actions.present?
.inline
.dropdown
- %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+ %a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
= custom_icon('icon_play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index ff250eeca50..170d786ecbf 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -1,4 +1,4 @@
-%div.branch-commit
+.branch-commit
- if deployment.ref
.icon-container
= deployment.tag? ? icon('tag') : icon('code-fork')
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index 779c8ea0104..52a1ece7d60 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -9,8 +9,8 @@
- if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed?
- - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path))
- .nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
+ - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
+ .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } }
This diff is collapsed.
%a.click-to-expand
Click to expand it.
@@ -25,7 +25,7 @@
- elsif diff_file.renamed_file
.nothing-here-block File moved
- elsif blob.image?
- - old_blob = diff_file.old_blob(diff_commit)
+ - old_blob = diff_file.old_blob(diff_file.old_content_commit || @base_commit)
= render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob
- else
.nothing-here-block No preview for this file type
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 067cf595da3..58c20e225c6 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -18,15 +18,16 @@
= parallel_diff_btn
= render 'projects/diffs/stats', diff_files: diff_files
-- if diff_files.overflow?
- = render 'projects/diffs/warning', diff_files: diff_files
+- if render_overflow_warning?(diff_files)
+ = render 'projects/diffs/warning', diff_files: diffs
.files{ data: { can_create_note: can_create_note } }
- - diff_files.each_with_index do |diff_file, index|
+ - diff_files.each_with_index do |diff_file|
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
- next unless blob
- blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw?
+ - file_hash = hexdigest(diff_file.file_path)
- = render 'projects/diffs/file', index: index, project: diffs.project,
+ = render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
diff_file: diff_file, diff_commit: diff_commit, blob: blob
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 8f4f9ad4a80..c37a33bbcd5 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,6 +1,6 @@
-.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)}
- .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"}
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{index}"
+.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
+ .file-title
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
- unless diff_file.submodule?
.file-actions.hidden-xs
@@ -9,7 +9,7 @@
= icon('comment')
\
- if editable_diff?(diff_file)
- - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
+ - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {}
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index d3ed8e1bf38..90c9a0c6c2b 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -21,7 +21,7 @@
- if diff_file.deleted_file
deleted
- = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy filename to clipboard')
+ = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
- if diff_file.mode_changed?
%small
diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml
index 9ec6a7aa5cd..1bccaaf5273 100644
--- a/app/views/projects/diffs/_image.html.haml
+++ b/app/views/projects/diffs/_image.html.haml
@@ -7,16 +7,16 @@
- if diff.renamed_file || diff.new_file || diff.deleted_file
.image
%span.wrap
- .frame{class: image_diff_class(diff)}
- %img{src: diff.deleted_file ? old_file_raw_path : file_raw_path}
+ .frame{ class: image_diff_class(diff) }
+ %img{ src: diff.deleted_file ? old_file_raw_path : file_raw_path, alt: diff.new_path }
%p.image-info= "#{number_to_human_size file.size}"
- else
.image
- %div.two-up.view
+ .two-up.view
%span.wrap
.frame.deleted
- %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))}
- %img{src: old_file_raw_path}
+ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) }
+ %img{ src: old_file_raw_path, alt: diff.old_path }
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size old_file.size}"
|
@@ -27,8 +27,8 @@
%span.meta-height
%span.wrap
.frame.added
- %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))}
- %img{src: file_raw_path}
+ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) }
+ %img{ src: file_raw_path, alt: diff.new_path }
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size file.size}"
|
@@ -38,32 +38,32 @@
%b H:
%span.meta-height
- %div.swipe.view.hide
+ .swipe.view.hide
.swipe-frame
.frame.deleted
- %img{src: old_file_raw_path}
+ %img{ src: old_file_raw_path, alt: diff.old_path }
.swipe-wrap
.frame.added
- %img{src: file_raw_path}
+ %img{ src: file_raw_path, alt: diff.new_path }
%span.swipe-bar
%span.top-handle
%span.bottom-handle
- %div.onion-skin.view.hide
+ .onion-skin.view.hide
.onion-skin-frame
.frame.deleted
- %img{src: old_file_raw_path}
+ %img{ src: old_file_raw_path, alt: diff.old_path }
.frame.added
- %img{src: file_raw_path}
+ %img{ src: file_raw_path, alt: diff.new_path }
.controls
.transparent
.drag-track
- .dragger{:style => "left: 0px;"}
+ .dragger{ :style => "left: 0px;" }
.opaque
.view-modes.hide
%ul.view-modes-menu
- %li.two-up{data: {mode: 'two-up'}} 2-up
- %li.swipe{data: {mode: 'swipe'}} Swipe
- %li.onion-skin{data: {mode: 'onion-skin'}} Onion skin
+ %li.two-up{ data: { mode: 'two-up' } } 2-up
+ %li.swipe{ data: { mode: 'swipe' } } Swipe
+ %li.onion-skin{ data: { mode: 'onion-skin' } } Onion skin
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 7042e9f1fc9..cd18ba2ed00 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -16,18 +16,18 @@
- if plain
= link_text
- else
- %a{href: "##{line_code}", data: { linenumber: link_text }}
+ %a{ href: "##{line_code}", data: { linenumber: link_text } }
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? " " : line.new_pos
- if plain
= link_text
- else
- %a{href: "##{line_code}", data: { linenumber: link_text }}
+ %a{ href: "##{line_code}", data: { linenumber: link_text } }
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
- if email
- %pre= diff_line_content(line.text, type)
+ %pre= line.text
- else
- = diff_line_content(line.text, type)
+ = diff_line_content(line.text)
- discussions = local_assigns.fetch(:discussions, nil)
- if discussions && !line.meta?
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 78aa9fb7391..b087485aa17 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,5 +1,5 @@
/ Side-by-side diff view
-%div.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
+.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table
- last_line = 0
- diff_file.parallel_diff_lines.each do |line|
@@ -13,9 +13,9 @@
- else
- left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left)
- %td.old_line.diff-line-num{id: left_line_code, class: left.type, data: { linenumber: left.old_pos }}
- %a{href: "##{left_line_code}" }= raw(left.old_pos)
- %td.line_content.parallel.noteable_line{class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old')}= diff_line_content(left.text)
+ %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
+ %a{ href: "##{left_line_code}" }= raw(left.old_pos)
+ %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text)
- else
%td.old_line.diff-line-num.empty-cell
%td.line_content.parallel
@@ -26,9 +26,9 @@
- else
- right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right)
- %td.new_line.diff-line-num{id: right_line_code, class: right.type, data: { linenumber: right.new_pos }}
- %a{href: "##{right_line_code}" }= raw(right.new_pos)
- %td.line_content.parallel.noteable_line{class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new')}= diff_line_content(right.text)
+ %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
+ %a{ href: "##{right_line_code}" }= raw(right.new_pos)
+ %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text)
- else
%td.old_line.diff-line-num.empty-cell
%td.line_content.parallel
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index e751dabdf99..290f696d582 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -9,28 +9,28 @@
%strong.cred #{diff_files.sum(&:removed_lines)} deletions
.file-stats.js-toggle-content.hide
%ul
- - diff_files.each_with_index do |diff_file, i|
+ - diff_files.each do |diff_file|
+ - file_hash = hexdigest(diff_file.file_path)
%li
- if diff_file.deleted_file
%span.deleted-file
- %a{href: "#diff-#{i}"}
+ %a{ href: "##{file_hash}" }
%i.fa.fa-minus
= diff_file.old_path
- elsif diff_file.renamed_file
%span.renamed-file
- %a{href: "#diff-#{i}"}
+ %a{ href: "##{file_hash}" }
%i.fa.fa-minus
= diff_file.old_path
&rarr;
= diff_file.new_path
- elsif diff_file.new_file
%span.new-file
- %a{href: "#diff-#{i}"}
+ %a{ href: "##{file_hash}" }
%i.fa.fa-plus
= diff_file.new_path
- else
%span.edit-file
- %a{href: "#diff-#{i}"}
+ %a{ href: "##{file_hash}" }
%i.fa.fa-adjust
= diff_file.new_path
-
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 0aa8801c2d8..1d7fdf68cb3 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -21,85 +21,78 @@
.form-group
= f.label :default_branch, "Default Branch", class: 'label-light'
= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
- .form-group.project-visibility-level-holder
- = f.label :visibility_level, class: 'label-light' do
- Visibility Level
- = link_to "(?)", help_page_path("public_access/public_access")
- - if can_change_visibility_level?(@project, current_user)
- = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project)
- - else
- .info
- = visibility_level_icon(@project.visibility_level)
- %strong
- = visibility_level_label(@project.visibility_level)
- .light= visibility_level_description(@project.visibility_level, @project)
-
- .form-group
- = render 'shared/allow_request_access', form: f
-
.form-group
= f.label :tag_list, "Tags", class: 'label-light'
= f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas.
%hr
- %fieldset.features.append-bottom-0
+ %fieldset.append-bottom-0
%h5.prepend-top-0
- Feature Visibility
-
- = f.fields_for :project_feature do |feature_fields|
- .form_group.prepend-top-20
- .row
- .col-md-9
- = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
- %span.help-block Push files to be stored in this project
- .col-md-3.js-repo-access-level
- = project_feature_access_select(:repository_access_level)
+ Sharing &amp; Permissions
+ .form_group.prepend-top-20.sharing-and-permissions
+ .row.js-visibility-select
+ .col-md-9
+ %label.label-light
+ = label_tag :project_visibility, 'Project Visibility', class: 'label-light'
+ = link_to "(?)", help_page_path("public_access/public_access")
+ %span.help-block
+ .col-md-3.visibility-select-container
+ = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
+ = f.fields_for :project_feature do |feature_fields|
+ %fieldset.features
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
+ %span.help-block View and edit files in this project
+ .col-md-3.js-repo-access-level
+ = project_feature_access_select(:repository_access_level)
- .col-sm-12
- .row
- .col-md-9.project-feature-nested
- = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
- %span.help-block Submit changes to be merged upstream
- .col-md-3
- = project_feature_access_select(:merge_requests_access_level)
+ .row
+ .col-md-9.project-feature.nested
+ = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
+ %span.help-block Submit changes to be merged upstream
+ .col-md-3
+ = project_feature_access_select(:merge_requests_access_level)
- .row
- .col-md-9.project-feature-nested
- = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
- %span.help-block Submit, test and deploy your changes before merge
- .col-md-3
- = project_feature_access_select(:builds_access_level)
+ .row
+ .col-md-9.project-feature.nested
+ = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
+ %span.help-block Submit, test and deploy your changes before merge
+ .col-md-3
+ = project_feature_access_select(:builds_access_level)
- .row
- .col-md-9
- = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
- %span.help-block Share code pastes with others out of Git repository
- .col-md-3
- = project_feature_access_select(:snippets_access_level)
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
+ %span.help-block Share code pastes with others out of Git repository
+ .col-md-3
+ = project_feature_access_select(:snippets_access_level)
- .row
- .col-md-9
- = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
- %span.help-block Lightweight issue tracking system for this project
- .col-md-3
- = project_feature_access_select(:issues_access_level)
-
- .row
- .col-md-9
- = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
- %span.help-block Pages for project documentation
- .col-md-3
- = project_feature_access_select(:wiki_access_level)
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
+ %span.help-block Lightweight issue tracking system for this project
+ .col-md-3
+ = project_feature_access_select(:issues_access_level)
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
+ %span.help-block Pages for project documentation
+ .col-md-3
+ = project_feature_access_select(:wiki_access_level)
+ .form-group
+ = render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin?
- .checkbox
- = f.label :lfs_enabled do
- = f.check_box :lfs_enabled
- %strong LFS
- %br
- %span.descr
+ .row
+ .col-md-9
+ = f.label :lfs_enabled, 'LFS', class: 'label-light'
+ %span.help-block
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ .col-md-3
+ = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' }
+
- if Gitlab.config.registry.enabled
.form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) }
@@ -111,7 +104,8 @@
%span.descr Enable Container Registry for this project
= link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
- = render 'merge_request_settings', f: f
+ = render 'merge_request_settings', form: f
+
%hr
%fieldset.features.append-bottom-default
%h5.prepend-top-0
@@ -144,7 +138,7 @@
such as compressing file revisions and removing unreachable objects.
.col-lg-9
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
- method: :post, class: "btn btn-save"
+ method: :post, class: "btn btn-default"
%hr
.row.prepend-top-default
.col-lg-3
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 7a39064adc5..58c085cdb9d 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -16,7 +16,7 @@
%p
Otherwise you can start with adding a
= succeed ',' do
- = link_to "README", new_readme_path, class: 'underlined-link'
+ = link_to "README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link'
a
= succeed ',' do
= link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link'
@@ -32,7 +32,7 @@
.empty_wrapper
%h3.page-title-empty
Command line instructions
- %div.git-empty
+ .git-empty
%fieldset
%h5 Git global setup
%pre.light-well
@@ -52,7 +52,7 @@
git push -u origin master
%fieldset
- %h5 Existing folder or Git repository
+ %h5 Existing folder
%pre.light-well
:preserve
cd existing_folder
@@ -62,6 +62,15 @@
git commit
git push -u origin master
+ %fieldset
+ %h5 Existing Git repository
+ %pre.light-well
+ :preserve
+ cd existing_repo
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git push -u origin --all
+ git push -u origin --tags
+
- if can? current_user, :remove_project, @project
.prepend-top-20
= link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
deleted file mode 100644
index b75d5df4150..00000000000
--- a/app/views/projects/environments/_environment.html.haml
+++ /dev/null
@@ -1,35 +0,0 @@
-- last_deployment = environment.last_deployment
-
-%tr.environment
- %td
- = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
-
- %td.deployment-column
- - if last_deployment
- %span ##{last_deployment.iid}
- - if last_deployment.user
- by
- = user_avatar(user: last_deployment.user, size: 20)
-
- %td
- - if last_deployment && last_deployment.deployable
- = link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do
- = "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})"
-
- %td
- - if last_deployment
- = render 'projects/deployments/commit', deployment: last_deployment
- - else
- %p.commit-title
- No deployments yet
-
- %td
- - if last_deployment
- #{time_ago_with_tooltip(last_deployment.created_at)}
-
- %td.hidden-xs
- .pull-right
- = render 'projects/environments/external_url', environment: environment
- = render 'projects/deployments/actions', deployment: last_deployment
- = render 'projects/environments/stop', environment: environment
- = render 'projects/deployments/rollback', deployment: last_deployment
diff --git a/app/views/projects/environments/_header_title.html.haml b/app/views/projects/environments/_header_title.html.haml
deleted file mode 100644
index e056fccad5d..00000000000
--- a/app/views/projects/environments/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Environments", project_environments_path(@project))
diff --git a/app/views/projects/environments/_terminal_button.html.haml b/app/views/projects/environments/_terminal_button.html.haml
new file mode 100644
index 00000000000..97de9c95de7
--- /dev/null
+++ b/app/views/projects/environments/_terminal_button.html.haml
@@ -0,0 +1,3 @@
+- if environment.has_terminals? && can?(current_user, :admin_environment, @project)
+ = link_to terminal_namespace_project_environment_path(@project.namespace, @project, environment), class: 'btn terminal-button' do
+ = icon('terminal')
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 8f555afcf11..8c728eb0f6a 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -2,47 +2,18 @@
- page_title "Environments"
= render "projects/pipelines/head"
-%div{ class: container_class }
- .top-area
- %ul.nav-links
- %li{class: ('active' if @scope.nil?)}
- = link_to project_environments_path(@project) do
- Available
- %span.badge.js-available-environments-count
- = number_with_delimiter(@all_environments.available.count)
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag("environments/environments_bundle.js")
- %li{class: ('active' if @scope == 'stopped')}
- = link_to project_environments_path(@project, scope: :stopped) do
- Stopped
- %span.badge.js-stopped-environments-count
- = number_with_delimiter(@all_environments.stopped.count)
-
- - if can?(current_user, :create_environment, @project) && !@all_environments.blank?
- .nav-controls
- = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
- New environment
-
- .environments-container
- - if @all_environments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any environments right now.
- %p.blank-state-text
- Environments are places where code gets deployed, such as staging or production.
- %br
- = succeed "." do
- = link_to "Read more about environments", help_page_path("ci/environments")
- - if can?(current_user, :create_environment, @project)
- = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
- New environment
- - else
- .table-holder
- %table.table.ci-table.environments
- %tbody
- %th Environment
- %th Last Deployment
- %th Build
- %th Commit
- %th
- %th.hidden-xs
- = render @environments
+#environments-list-view{ data: { environments_data: environments_list_data,
+ "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
+ "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
+ "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
+ "project-environments-path" => project_environments_path(@project),
+ "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
+ "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project),
+ "help-page-path" => help_page_path("ci/environments"),
+ "css-class" => container_class,
+ "commit-icon-svg" => custom_icon("icon_commit"),
+ "terminal-icon-svg" => custom_icon("icon_terminal"),
+ "play-icon-svg" => custom_icon("icon_play") } }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 992d98cdd96..da06ae1c313 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -8,6 +8,7 @@
%h3.page-title= @environment.name.capitalize
.col-md-3
.nav-controls
+ = render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
new file mode 100644
index 00000000000..431253c1299
--- /dev/null
+++ b/app/views/projects/environments/terminal.html.haml
@@ -0,0 +1,22 @@
+- @no_container = true
+- page_title "Terminal for environment", @environment.name
+= render "projects/pipelines/head"
+
+- content_for :page_specific_javascripts do
+ = stylesheet_link_tag "xterm/xterm"
+ = page_specific_javascript_tag("terminal/terminal_bundle.js")
+
+%div{ class: container_class }
+ .top-area
+ .row
+ .col-sm-6
+ %h3.page-title
+ Terminal for environment
+ = @environment.name
+
+ .col-sm-6
+ .nav-controls
+ = render 'projects/deployments/actions', deployment: @environment.last_deployment
+
+.terminal-container{ class: container_class }
+ #terminal{ data: { project_path: "#{terminal_namespace_project_environment_path(@project.namespace, @project, @environment)}.ws" } }
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 9322c82904f..4cdb44325b3 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -9,11 +9,11 @@
= link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
= @project.path
%li.file-finder
- %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path', autocomplete: 'off'}
+ %input#file_find.form-control.file-finder-input{ type: "text", placeholder: 'Find by path', autocomplete: 'off' }
- %div.tree-content-holder
+ .tree-content-holder
.table-holder
- %table.table.files-slider{class: "table_#{@hex_path} tree-table table-striped" }
+ %table.table.files-slider{ class: "table_#{@hex_path} tree-table table-striped" }
%tbody
= spinner nil, true
diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml
index 3d0ab5b85d6..98d81308407 100644
--- a/app/views/projects/forks/error.html.haml
+++ b/app/views/projects/forks/error.html.haml
@@ -13,7 +13,11 @@
- if @forked_project && @forked_project.errors.any?
%p
&ndash;
- = @forked_project.errors.full_messages.first
+ - error = @forked_project.errors.full_messages.first
+ - if error.include?("already been taken")
+ Name has already been taken
+ - else
+ = error
%p
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork", class: "btn" do
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index abf4f697f86..6c8a6f051a9 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -1,7 +1,7 @@
.top-area
.nav-text
- full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private"
- == #{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
+ = "#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}"
.nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
@@ -9,13 +9,13 @@
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
.dropdown
- %button.dropdown-toggle.btn.sort-forks{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light sort:
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
- excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 80fe6be49b0..6d7af1685fd 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -1,39 +1,67 @@
-%tr.generic_commit_status
+- admin = local_assigns.fetch(:admin, false)
+- ref = local_assigns.fetch(:ref, nil)
+- commit_sha = local_assigns.fetch(:commit_sha, nil)
+- retried = local_assigns.fetch(:retried, false)
+- pipeline_link = local_assigns.fetch(:pipeline_link, false)
+- stage = local_assigns.fetch(:stage, false)
+- coverage = local_assigns.fetch(:coverage, false)
+
+%tr.generic_commit_status{ class: ('retried' if retried) }
%td.status
- - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
- = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url)
- - else
- = ci_status_with_icon(generic_commit_status.status)
+ = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user)
%td.generic_commit_status-link
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
= link_to generic_commit_status.target_url do
- %strong ##{generic_commit_status.id}
+ %span.build-link ##{generic_commit_status.id}
- else
- %strong ##{generic_commit_status.id}
+ %span.build-link ##{generic_commit_status.id}
+
+ - if ref
+ - if generic_commit_status.ref
+ .icon-container
+ = generic_commit_status.tags.any? ? icon('tag') : icon('code-fork')
+ = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
+ - else
+ .light none
+ .icon-container.commit-icon
+ = custom_icon("icon_commit")
+
+ - if commit_sha
+ = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "commit-id monospace"
- - if defined?(retried) && retried
+ - if retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.')
- - if defined?(commit_sha) && commit_sha
- %td
- = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
+ .label-container
+ - if generic_commit_status.tags.any?
+ - generic_commit_status.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if retried
+ %span.label.label-warning retried
- - if defined?(ref) && ref
+ - if pipeline_link
%td
- - if generic_commit_status.ref
- = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref)
+ = link_to pipeline_path(generic_commit_status.pipeline) do
+ %span.pipeline-id ##{generic_commit_status.pipeline.id}
+ %span by
+ - if generic_commit_status.pipeline.user
+ = user_avatar(user: generic_commit_status.pipeline.user, size: 20)
- else
- .light none
+ %span.monospace API
- - if defined?(runner) && runner
+ - if admin
+ %td
+ - if generic_commit_status.project
+ = link_to generic_commit_status.project.name_with_namespace, admin_namespace_project_path(generic_commit_status.project.namespace, generic_commit_status.project)
%td
- if generic_commit_status.try(:runner)
= runner_link(generic_commit_status.runner)
- else
.light none
- - if defined?(stage) && stage
+ - if stage
%td
= generic_commit_status.stage
@@ -41,24 +69,19 @@
= generic_commit_status.name
%td
- - if generic_commit_status.tags.any?
- - generic_commit_status.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if defined?(retried) && retried
- %span.label.label-warning retried
-
- %td.duration
- if generic_commit_status.duration
- = icon("clock-o")
- = time_interval_in_words(generic_commit_status.duration)
+ %p.duration
+ = custom_icon("icon_timer")
+ = duration_in_numbers(generic_commit_status.duration)
- %td.timestamp
- if generic_commit_status.finished_at
- = icon("calendar")
- %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
+ %p.finished-at
+ = icon("calendar")
+ %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
- - if defined?(coverage) && coverage
- %td.coverage
- - if generic_commit_status.try(:coverage)
- #{generic_commit_status.coverage}%
+ %td.coverage
+ - if coverage && generic_commit_status.try(:coverage)
+ #{generic_commit_status.coverage}%
+
+ %td
+ -# empty column to match number of columns in ci/builds/_build.html.haml
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
deleted file mode 100644
index 1c457244a7a..00000000000
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } }
- - if subject.target_url
- = link_to subject.target_url do
- %span.ci-status-icon
- = ci_icon_for_status(subject.status)
- %span.ci-status-text= subject.name
- - else
- %span.ci-status-icon
- = ci_icon_for_status(subject.status)
- %span.ci-status-text= subject.name
diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml
index 195f18afc76..bb0975a9535 100644
--- a/app/views/projects/graphs/ci/_build_times.haml
+++ b/app/views/projects/graphs/ci/_build_times.haml
@@ -2,7 +2,7 @@
%p.light
Commit duration in minutes for last 30 commits
- %canvas#build_timesChart{height: 200}
+ %canvas#build_timesChart{ height: 200 }
:javascript
var data = {
diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml
index 1fbf6ca2c1c..431657c4dcb 100644
--- a/app/views/projects/graphs/ci/_builds.haml
+++ b/app/views/projects/graphs/ci/_builds.haml
@@ -13,18 +13,18 @@
%p.light
Builds for last week
(#{date_from_to(Date.today - 7.days, Date.today)})
- %canvas#weekChart{height: 200}
+ %canvas#weekChart{ height: 200 }
.prepend-top-default
%p.light
Builds for last month
(#{date_from_to(Date.today - 30.days, Date.today)})
- %canvas#monthChart{height: 200}
+ %canvas#monthChart{ height: 200 }
.prepend-top-default
%p.light
Builds for last year
- %canvas#yearChart.padded{height: 250}
+ %canvas#yearChart.padded{ height: 250 }
- [:week, :month, :year].each do |scope|
:javascript
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index ac5f792d140..5ebb939a109 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -21,7 +21,7 @@
%h3#date_header.page-title
%p.light
Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits.
- %input#brush_change{:type => "hidden"}
+ %input#brush_change{ :type => "hidden" }
.graphs.row
#contributors-master
#contributors.clearfix
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/_index.html.haml
index 1b0dbbb8111..99d0df2ac34 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/_index.html.haml
@@ -20,10 +20,10 @@
.form-group
= label_tag :expires_at, 'Access expiration date', class: 'label-light'
.clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups'
%i.clear-icon.js-clear-input
.help-block
- On this date, all users in the group will automatically lose access to this project.
+ On this date, all members in the group will automatically lose access to this project.
= submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3
%hr
diff --git a/app/views/projects/group_links/update.js.haml b/app/views/projects/group_links/update.js.haml
index af9a5b19060..55520fda494 100644
--- a/app/views/projects/group_links/update.js.haml
+++ b/app/views/projects/group_links/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}');
$("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("#group_member_#{@group_link.id}"));
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/_index.html.haml
index 8faad351463..8faad351463 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/_index.html.haml
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 4d8ee562e6a..c52b3860636 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,4 +1,4 @@
-- page_title "Import in progress"
+- page_title @project.forked? ? "Forking in progress" : "Import in progress"
.save-project-loader
.center
%h2
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index c80210d6ff4..bd46af339cf 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -43,7 +43,7 @@
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
- %span{class: "#{'cred' if issue.overdue?}"}
+ %span{ class: "#{'cred' if issue.overdue?}" }
&nbsp;
= icon('calendar')
= issue.due_date.to_s(:medium)
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index a4b752ad86d..34d5a3e1831 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,8 +1,7 @@
%ul.content-list.issues-list.issuable-list
= render partial: "projects/issues/issue", collection: @issues
- if @issues.blank?
- %li
- .nothing-here-block No issues to show
+ = render 'shared/empty_states/issues'
- if @issues.present?
= paginate @issues, theme: "gitlab"
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 31d3ec23276..d48923b422a 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -2,12 +2,12 @@
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list.related-merge-requests
- - has_any_ci = @merge_requests.any?(&:pipeline)
+ - has_any_ci = @merge_requests.any?(&:head_pipeline)
- @merge_requests.each do |merge_request|
%li
%span.merge-request-ci-status
- - if merge_request.pipeline
- = render_pipeline_status(merge_request.pipeline)
+ - if merge_request.head_pipeline
+ = render_pipeline_status(merge_request.head_pipeline)
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
@@ -19,11 +19,17 @@
in
- project = merge_request.target_project
= link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
- %span.merge-request-status.prepend-left-10
- - if merge_request.merged?
- MERGED
- - elsif merge_request.closed?
- CLOSED
+
+ - if merge_request.merged?
+ %span.merge-request-status.prepend-left-10.merged
+ Merged
+ - elsif merge_request.closed?
+ %span.merge-request-status.prepend-left-10.closed
+ Closed
+ - else
+ %span.merge-request-status.prepend-left-10.open
+ Open
+
- if @closed_by_merge_requests.present?
%li
= render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index c56b6cc11f5..13e2150f997 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,9 +1,6 @@
- if can?(current_user, :push_code, @project)
.pull-right
- #new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
- = link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do
- = icon('spinner spin')
- Checking branches
+ #new-branch.new-branch{ 'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue) }
= link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid),
method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do
New branch
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index c493ff3585b..18e8372ecab 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -6,12 +6,15 @@
= content_for :sub_nav do
= render "projects/issues/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js')
+
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
-%div{ class: (container_class) }
- - if @project.issues.any?
+- if project_issues(@project).exists?
+ %div{ class: (container_class) }
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
@@ -20,7 +23,6 @@
= icon('rss')
%span.icon-label
Subscribe
- = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- if can? current_user, :create_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace,
@project,
@@ -30,27 +32,11 @@
title: "New Issue",
id: "new_issue_link" do
New Issue
- = render 'shared/issuable/filter', type: :issues
+ = render 'shared/issuable/search_bar', type: :issues
.issues-holder
= render 'issues'
- if new_issue_email
= render 'issue_by_email', email: new_issue_email
- - else
- .blank-state.blank-state-welcome
- %h2.blank-state-title.blank-state-welcome-title
- Welcome to GitLab Issues
- %p.blank-state-text
- Code, test, and deploy together
- .blank-state
- .blank-state-icon
- = custom_icon("issues", size: 50)
- %h3.blank-state-title
- You don't have any issues right now.
- %p.blank-state-text
- Issues are the best way to track your project progress
- - if can? current_user, :create_issue, @project
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
- New Issue
- - if new_issue_email
- = render 'issue_by_email', email: new_issue_email
+- else
+ = render 'shared/empty_states/issues', button_path: new_namespace_project_issue_path(@project.namespace, @project)
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index bd629b5c519..9fa00811af0 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,6 +1,9 @@
+- @content_class = "limit-container-width" unless fluid_layout
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/vue_resource.js')
.clearfix.detail-page-header
.issuable-header
diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml
new file mode 100644
index 00000000000..605c7f61dee
--- /dev/null
+++ b/app/views/projects/mattermosts/_no_teams.html.haml
@@ -0,0 +1,12 @@
+%p
+ You aren’t a member of any team on the Mattermost instance at
+ %strong= Gitlab.config.mattermost.host
+%p
+ To install this service,
+ = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do
+ join a team
+ = icon('external-link')
+ and try again.
+%hr
+.clearfix
+ = link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right'
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
new file mode 100644
index 00000000000..a80f9aa4c4a
--- /dev/null
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -0,0 +1,47 @@
+%p
+ This service will be installed on the Mattermost instance at
+ %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
+%hr
+= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f|
+ %h4 Team
+ %p
+ = @teams.one? ? 'The team' : 'Select the team'
+ where the slash commands will be used in
+ - selected_id = @teams.one? ? @teams.keys.first : 0
+ - options = mattermost_teams_options(@teams)
+ - options = options_for_select(options, selected_id)
+ = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id })
+ = f.hidden_field(:team_id, value: selected_id) if @teams.one?
+ .help-block
+ - if @teams.one?
+ This is the only available team.
+ - else
+ The list shows all available teams.
+ To create a team,
+ = link_to "#{Gitlab.config.mattermost.host}/create_team" do
+ use Mattermost's interface
+ = icon('external-link')
+ or ask your Mattermost system administrator.
+ %hr
+ %h4 Command trigger word
+ %p Choose the word that will trigger commands
+ = f.text_field(:trigger, value: @project.path, class: 'form-control')
+ .help-block
+ %p
+ Trigger word must be unique, and can't begin with a slash or contain any spaces.
+ Use the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+ %p
+ Reserved:
+ = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do
+ see list of built-in slash commands
+ = icon('external-link')
+ %hr
+ .clearfix
+ .pull-right
+ = link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg'
+ = f.submit 'Install', class: 'btn btn-save btn-lg'
diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml
new file mode 100644
index 00000000000..96b1d2aee61
--- /dev/null
+++ b/app/views/projects/mattermosts/new.html.haml
@@ -0,0 +1,8 @@
+.service-installation
+ .inline.pull-right
+ = custom_icon('mattermost_logo', size: 48)
+ %h3 Install Mattermost Command
+ - if @teams.empty?
+ = render 'no_teams'
+ - else
+ = render 'team_selection'
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 12408068834..e3b0aa7e644 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,4 +1,4 @@
-%li{ class: mr_css_classes(merge_request) }
+%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
- if @bulk_edit
.issue-check
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
@@ -15,9 +15,9 @@
= icon('ban')
CLOSED
- - if merge_request.pipeline
+ - if merge_request.head_pipeline
%li
- = render_pipeline_status(merge_request.pipeline)
+ = render_pipeline_status(merge_request.head_pipeline)
- if merge_request.open? && merge_request.broken?
%li
@@ -39,7 +39,7 @@
= icon('thumbs-down')
= downvotes
- - note_count = merge_request.mr_and_commit_notes.user.count
+ - note_count = merge_request.related_notes.user.count
%li
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
= icon('comments')
@@ -54,15 +54,18 @@
= link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
= icon('code-fork')
= merge_request.target_branch
+
- if merge_request.milestone
&nbsp;
= link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
= icon('clock-o')
= merge_request.milestone.title
+
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
= link_to_label(label, subject: merge_request.project, type: :merge_request)
+
- if merge_request.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 9c6f562f7db..36c6e7a8dad 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -10,8 +10,8 @@
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f|
- = render 'shared/issuable/form', f: f, issuable: @merge_request
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
+ = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
= f.hidden_field :target_project_id
@@ -34,10 +34,6 @@
= link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines
%span.badge= @pipelines.size
- %li.builds-tab
- = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
- Builds
- %span.badge= @statuses.size
%li.diffs-tab
= link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
Changes
@@ -49,8 +45,6 @@
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
- if @pipelines.any?
- #builds.builds.tab-pane
- = render "projects/merge_requests/show/builds"
#pipelines.pipelines.tab-pane
= render "projects/merge_requests/show/pipelines"
@@ -64,6 +58,5 @@
});
:javascript
var merge_request = new MergeRequest({
- action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
- buildsLoaded: "#{@pipelines.any? ? 'true' : 'false'}"
+ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}"
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index f57abe73977..2a7cd3a19d0 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,13 +1,15 @@
+- @content_class = "limit-container-width" unless fluid_layout
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/vue_resource.js')
= page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
-.merge-request{'data-url' => merge_request_path(@merge_request)}
+.merge-request{ 'data-url' => merge_request_path(@merge_request) }
= render "projects/merge_requests/show/mr_title"
- .merge-request-details.issuable-details{data: {id: @merge_request.project.id}}
+ .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
= render "projects/merge_requests/show/mr_box"
.append-bottom-default.mr-source-target.prepend-top-default
- if @merge_request.open?
@@ -31,7 +33,7 @@
%span.label-branch= source_branch_with_namespace(@merge_request)
%span into
%span.label-branch
- = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
+ = link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
- if @merge_request.open? && @merge_request.diverged_from_target_branch?
%span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
@@ -41,69 +43,63 @@
= render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
- .light.prepend-top-default.append-bottom-default
+ .merge-manually.light.prepend-top-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- - if @commits_count.nonzero?
- .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
- %div{ class: container_class }
- %ul.merge-request-tabs.nav-links.no-top.no-bottom
- %li.notes-tab
- = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
- Discussion
- %span.badge= @merge_request.mr_and_commit_notes.user.count
- - if @merge_request.source_project
- %li.commits-tab
- = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
- Commits
- %span.badge= @commits_count
- - if @pipeline
- %li.pipelines-tab
- = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
- Pipelines
- %span.badge= @pipelines.size
- %li.builds-tab
- = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
- Builds
- %span.badge= @statuses.size
- %li.diffs-tab
- = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
- Changes
- %span.badge= @merge_request.diff_size
- %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
- %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
+ .content-block.content-block-small.emoji-list-container
+ = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
+
+ .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
+ %div{ class: container_class }
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
+ %li.notes-tab
+ = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
+ Discussion
+ %span.badge= @merge_request.related_notes.user.count
+ - if @merge_request.source_project
+ %li.commits-tab
+ = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
+ Commits
+ %span.badge= @commits_count
+ - if @pipelines.any?
+ %li.pipelines-tab
+ = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
+ Pipelines
+ %span.badge= @pipelines.size
+ %li.diffs-tab
+ = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
+ Changes
+ %span.badge= @merge_request.diff_size
+ %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
+ %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
+ %div
.line-resolve-all{ "v-show" => "discussionCount > 0",
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
%span.line-resolve-btn.is-disabled{ type: "button",
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
= render "shared/icons/icon_status_success.svg"
%span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
= render "discussions/jump_to_next"
- .tab-content#diff-notes-app
- #notes.notes.tab-pane.voting_notes
- .content-block.content-block-small
- = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
-
- .row
- %section.col-md-12
- .issuable-discussion
- = render "projects/merge_requests/discussion"
+ .tab-content#diff-notes-app
+ #notes.notes.tab-pane.voting_notes
+ .row
+ %section.col-md-12
+ .issuable-discussion
+ = render "projects/merge_requests/discussion"
- #commits.commits.tab-pane
- - # This tab is always loaded via AJAX
- #builds.builds.tab-pane
- - # This tab is always loaded via AJAX
- #pipelines.pipelines.tab-pane
- - # This tab is always loaded via AJAX
- #diffs.diffs.tab-pane
- - # This tab is always loaded via AJAX
+ #commits.commits.tab-pane
+ - # This tab is always loaded via AJAX
+ #pipelines.pipelines.tab-pane
+ - # This tab is always loaded via AJAX
+ #diffs.diffs.tab-pane
+ - # This tab is always loaded via AJAX
- .mr-loading-status
- = spinner
+ .mr-loading-status
+ = spinner
= render 'shared/issuable/sidebar', issuable: @merge_request
- if @merge_request.can_be_reverted?(current_user)
@@ -117,3 +113,6 @@
merge_request = new MergeRequest({
action: "#{controller.action_name}"
});
+
+ var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}";
+
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index d9f74d2cbfb..ebef2157d34 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,5 +1,6 @@
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/vue_resource.js')
= page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/show/mr_title"
@@ -9,32 +10,29 @@
= render 'shared/issuable/sidebar', issuable: @merge_request
-#conflicts{"v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json),
+#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json),
resolve_conflicts_path: resolve_conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request) } }
- .loading{"v-if" => "isLoading"}
+ .loading{ "v-if" => "isLoading" }
%i.fa.fa-spinner.fa-spin
- .nothing-here-block{"v-if" => "hasError"}
+ .nothing-here-block{ "v-if" => "hasError" }
{{conflictsData.errorMessage}}
= render partial: "projects/merge_requests/conflicts/commit_stats"
- .files-wrapper{"v-if" => "!isLoading && !hasError"}
+ .files-wrapper{ "v-if" => "!isLoading && !hasError" }
.files
- .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"}
+ .diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" }
.file-title
- %i.fa.fa-fw{":class" => "file.iconClass"}
+ %i.fa.fa-fw{ ":class" => "file.iconClass" }
%strong {{file.filePath}}
= render partial: 'projects/merge_requests/conflicts/file_actions'
.diff-content.diff-wrap-lines
- .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
+ .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
- .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
- = render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines"
- %div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"}
+ .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
+ %parallel-conflict-lines{ ":file" => "file" }
+ %div{ "v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'" }
= render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
= render partial: "projects/merge_requests/conflicts/submit_form"
-
--# Components
-= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
index 5ab3cd96163..964dc40a213 100644
--- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -1,9 +1,9 @@
-.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
- .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"}
+.content-block.oneline-block.files-changed{ "v-if" => "!isLoading && !hasError" }
+ .inline-parallel-buttons{ "v-if" => "showDiffViewTypeSwitcher" }
.btn-group
- %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"}
+ %button.btn{ ":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')" }
Inline
- %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"}
+ %button.btn{ ":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')" }
Side-by-side
.js-toggle-container
diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
index 05af57acf03..2595ce74ac0 100644
--- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
@@ -1,5 +1,5 @@
.file-actions
- .btn-group{"v-if" => "file.type === 'text'"}
+ .btn-group{ "v-if" => "file.type === 'text'" }
%button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
'@click' => "onClickResolveModeButton(file, 'interactive')",
type: 'button' }
@@ -8,5 +8,5 @@
'@click' => "onClickResolveModeButton(file, 'edit')",
type: 'button' }
Edit inline
- %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+ %a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" }
View file @{{conflictsData.shortCommitSha}}
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
index 6ffaa9ad4d2..62c9748c510 100644
--- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -10,7 +10,7 @@
.col-sm-offset-2.col-sm-10
.row
.col-xs-6
- %button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
+ %button.btn.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
%span {{commitButtonText}}
.col-xs-6.text-right
= link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
diff --git a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
index 3c927d362c2..aff3fb82fa6 100644
--- a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
+++ b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
@@ -1,4 +1,4 @@
-%diff-file-editor{"inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation"}
+%diff-file-editor{ "inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation" }
.diff-editor-wrap{ "v-show" => "file.showEditor" }
.discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" }
.discard-changes-alert
diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
index f094df7fcaa..d828cb6cf9e 100644
--- a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
+++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
@@ -1,15 +1,14 @@
-%inline-conflict-lines{ "inline-template" => "true", ":file" => "file"}
+%inline-conflict-lines{ "inline-template" => "true", ":file" => "file" }
%table
- %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
- %td.diff-line-num.new_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ %tr.line_holder.diff-inline{ "v-for" => "line in file.inlineLines" }
+ %td.diff-line-num.new_line{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader" }
%a {{line.new_line}}
- %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
+ %td.diff-line-num.old_line{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader" }
%a {{line.old_line}}
- %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
- {{{line.richText}}}
- %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %strong {{{line.richText}}}
+ %td.line_content{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader", "v-html" => "line.richText" }
+ %td.diff-line-num.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" }
+ %td.diff-line-num.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" }
+ %td.line_content.header{ ":class" => "lineCssClass(line)", "v-if" => "line.isHeader" }
+ %strong{ "v-html" => "line.richText" }
%button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
{{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
deleted file mode 100644
index 5690bf7419c..00000000000
--- a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"}
- %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %strong {{line.richText}}
- %button.btn{"@click" => "handleSelected(file, line.id, line.section)"}
- {{line.buttonTitle}}
- %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
- {{line.lineNumber}}
- %td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
- {{{line.richText}}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
deleted file mode 100644
index a8ecdf59393..00000000000
--- a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"}
- %table
- %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
- %td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"}
diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml
deleted file mode 100644
index 808ef7fed27..00000000000
--- a/app/views/projects/merge_requests/show/_builds.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index a0e12fb3f38..11793919ff7 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1,6 +1,8 @@
-.content-block.oneline-block
- = icon("sort-amount-desc")
- Most recent commits displayed first
-
-%ol#commits-list.list-unstyled
- = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
+- if @commits.empty?
+ .commits-empty
+ %h4
+ There are no commits yet.
+ = custom_icon ('illustration_no_commits')
+- else
+ %ol#commits-list.list-unstyled
+ = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 99c71e1454a..5f048d04b27 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,13 +1,5 @@
-- if @merge_request_diff.collected?
+- if @merge_request_diff.collected? || @merge_request_diff.overflow?
= render 'projects/merge_requests/show/versions'
= render "projects/diffs/diffs", diffs: @diffs
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
-- else
- .alert.alert-warning
- %h4
- Changes view for this comparison is extremely large.
- %p
- You can
- = link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink"
- instead.
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index f1d5441f9dd..93ed4b68e0e 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -1,14 +1,14 @@
-%div#modal_merge_info.modal
+#modal_merge_info.modal
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3 Check out, review, and merge locally
.modal-body
%p
%strong Step 1.
Fetch and check out the branch for this merge request
- = clipboard_button(clipboard_target: "pre#merge-info-1")
+ = clipboard_button(clipboard_target: "pre#merge-info-1", title: "Copy commands to clipboard")
%pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
@@ -25,7 +25,7 @@
%p
%strong Step 3.
Merge the branch and fix any conflicts that come up
- = clipboard_button(clipboard_target: "pre#merge-info-3")
+ = clipboard_button(clipboard_target: "pre#merge-info-3", title: "Copy commands to clipboard")
%pre.dark#merge-info-3
- if @merge_request.for_fork?
:preserve
@@ -38,7 +38,7 @@
%p
%strong Step 4.
Push the result of the merge to GitLab
- = clipboard_button(clipboard_target: "pre#merge-info-4")
+ = clipboard_button(clipboard_target: "pre#merge-info-4", title: "Copy commands to clipboard")
%pre.dark#merge-info-4
:preserve
git push origin #{h @merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index ed23d06ee5e..683cb8a5a27 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -4,7 +4,7 @@
%div
- if @merge_request.description.present?
- .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
+ .description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' }
.wiki
= preserve do
= markdown_field(@merge_request, :description)
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index eab48b78cb3..b0f3c86fd21 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -1,6 +1,6 @@
- if @merge_request_diffs.size > 1
.mr-version-controls
- %div.mr-version-menus-container.content-block
+ .mr-version-menus-container.content-block
Changes between
%span.dropdown.inline.mr-version-dropdown
%a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} }
@@ -13,13 +13,13 @@
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Version:
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times', class: 'dropdown-menu-close-icon')
.dropdown-content
%ul
- @merge_request_diffs.each do |merge_request_diff|
%li
- = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+ = link_to merge_request_version_path(@project, @merge_request, merge_request_diff, @start_sha), class: ('is-active' if merge_request_diff == @merge_request_diff) do
%strong
- if merge_request_diff.latest?
latest version
@@ -27,7 +27,7 @@
version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
%small
- #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
+ #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)},
= time_ago_with_tooltip(merge_request_diff.created_at)
- if @merge_request_diff.base_commit_sha
@@ -43,7 +43,7 @@
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Compared with:
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times', class: 'dropdown-menu-close-icon')
.dropdown-content
%ul
diff --git a/app/views/projects/merge_requests/widget/_closed.html.haml b/app/views/projects/merge_requests/widget/_closed.html.haml
index f3cc0e7e8a1..15f47ecf210 100644
--- a/app/views/projects/merge_requests/widget/_closed.html.haml
+++ b/app/views/projects/merge_requests/widget/_closed.html.haml
@@ -6,7 +6,7 @@
- if @merge_request.closed_event
by #{link_to_member(@project, @merge_request.closed_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.closed_event.created_at)}
- %p
+ %p
= succeed '.' do
The changes were not merged into
%span.label-branch= @merge_request.target_branch
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index a82c846baa7..c80dc33058d 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,25 +1,23 @@
- if @pipeline
.mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status|
- .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
+ .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) }
= ci_icon_for_status(status)
%span
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
for
- - commit = @merge_request.diff_head_commit
= succeed "." do
= link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
%span.ci-coverage
- = link_to "View details", pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'pipelines'}
- elsif @merge_request.has_ci?
- # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
- - # Remove in later versions when services like Jenkins will set CI status via Commit status API
+ - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- %w[success skipped canceled failed running pending].each do |status|
- .ci_widget{class: "ci-#{status}", style: "display:none"}
+ .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" }
= ci_icon_for_status(status)
%span
CI build
@@ -29,18 +27,16 @@
= succeed "." do
= link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace"
%span.ci-coverage
- - if details_path = ci_build_details_path(@merge_request)
- = link_to "View details", details_path, :"data-no-turbolink" => "data-no-turbolink"
.ci_widget
= icon("spinner spin")
Checking CI status for #{@merge_request.diff_head_commit.short_id}&hellip;
- .ci_widget.ci-not_found{style: "display:none"}
+ .ci_widget.ci-not_found{ style: "display:none" }
= icon("times-circle")
Could not find CI status for #{@merge_request.diff_head_commit.short_id}.
- .ci_widget.ci-error{style: "display:none"}
+ .ci_widget.ci-error{ style: "display:none" }
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
index d836a253507..9eef011b591 100644
--- a/app/views/projects/merge_requests/widget/_merged_buttons.haml
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -5,10 +5,10 @@
- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked
.clearfix.merged-buttons
- if can_remove_source_branch
- = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do
+ = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default remove_source_branch" do
= icon('trash-o')
Remove Source Branch
- if mr_can_be_reverted
- = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
+ = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning")
- if mr_can_be_cherry_picked
- = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
+ = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default")
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index 01314eb37d0..c0d6ab669b8 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -9,10 +9,10 @@
- if @project.archived?
= render 'projects/merge_requests/widget/open/archived'
- - elsif @merge_request.commits.blank?
- = render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.branch_missing?
= render 'projects/merge_requests/widget/open/missing_branch'
+ - elsif @merge_request.has_no_commits?
+ = render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.unchecked?
= render 'projects/merge_requests/widget/open/check'
- elsif @merge_request.cannot_be_merged? && !resolved_conflicts
@@ -23,18 +23,23 @@
= render 'projects/merge_requests/widget/open/merge_when_build_succeeds'
- elsif !@merge_request.can_be_merged_by?(current_user)
= render 'projects/merge_requests/widget/open/not_allowed'
- - elsif !@merge_request.mergeable_ci_state?
+ - elsif !@merge_request.mergeable_ci_state? && (@pipeline.failed? || @pipeline.canceled?)
= render 'projects/merge_requests/widget/open/build_failed'
- elsif !@merge_request.mergeable_discussions_state?
= render 'projects/merge_requests/widget/open/unresolved_discussions'
- elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept'
- - if mr_closes_issues.present?
+ - if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing.present?
.mr-widget-footer
%span
- %i.fa.fa-check
- Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
- = succeed '.' do
- != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
- = mr_assign_issues_link
+ = icon('check')
+ - if mr_closes_issues.present?
+ Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
+ = succeed '.' do
+ != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
+ = mr_assign_issues_link
+ - if mr_issues_mentioned_but_not_closing.present?
+ #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)}
+ != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author
+ #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not be closed.
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index 608fdf1c5f5..38328501ffd 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -14,7 +14,7 @@
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
- ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}",
+ ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
ci_message: {
normal: "Build {{status}} for \"{{title}}\"",
preparing: "{{status}} build for \"{{title}}\""
@@ -24,12 +24,10 @@
preparing: "{{status}} build",
normal: "Build {{status}}"
},
- builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
if (typeof merge_request_widget !== 'undefined') {
- clearInterval(merge_request_widget.fetchBuildStatusInterval);
merge_request_widget.cancelPolling();
merge_request_widget.clearEventListeners();
}
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index ce43ca3a286..7809e9c8c72 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
+
- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
@@ -9,7 +12,7 @@
- if @pipeline && @pipeline.active?
%span.btn-group
= button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do
- Merge When Build Succeeds
+ Merge When Pipeline Succeeds
- unless @project.only_allow_merge_if_build_succeeds?
= button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
= icon('caret-down')
@@ -19,7 +22,7 @@
%li
= link_to "#", class: "merge_when_build_succeeds" do
= icon('check fw')
- Merge When Build Succeeds
+ Merge When Pipeline Succeeds
%li
= link_to "#", class: "accept_merge_request" do
= icon('warning fw')
@@ -41,25 +44,9 @@
Modify commit message
.js-toggle-content.hide.prepend-top-default
= render 'shared/commit_message_container', params: params,
+ message_with_description: @merge_request.merge_commit_message(include_description: true),
+ message_without_description: @merge_request.merge_commit_message,
text: @merge_request.merge_commit_message,
rows: 14, hint: true
= hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off"
-
- :javascript
- $('.accept-mr-form').on('ajax:send', function() {
- $(".accept-mr-form :input").disable();
- });
-
- $('.accept_merge_request').on('click', function() {
- $('.js-merge-button').html("<i class='fa fa-spinner fa-spin'></i> Merge in progress");
- });
-
- $('.merge_when_build_succeeds').on('click', function() {
- $("#merge_when_build_succeeds").val("1");
- });
-
- $('.js-merge-dropdown a').on('click', function(e) {
- e.preventDefault();
- $(this).closest("form").submit();
- });
diff --git a/app/views/projects/merge_requests/widget/open/_archived.html.haml b/app/views/projects/merge_requests/widget/open/_archived.html.haml
index ab30fa6b243..0d61e56d8fb 100644
--- a/app/views/projects/merge_requests/widget/open/_archived.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_archived.html.haml
@@ -1,4 +1,4 @@
-%h4
+%h4
Project is archived
%p
This merge request cannot be merged because archived projects cannot be written to.
diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml
index e16878ba513..50086767446 100644
--- a/app/views/projects/merge_requests/widget/open/_check.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_check.html.haml
@@ -1,9 +1,6 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
+
%strong
= icon("spinner spin")
Checking ability to merge automatically&hellip;
-
-:javascript
- $(function() {
- merge_request_widget.getMergeStatus();
- });
-
diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
index af3096f04d9..c98b2c42597 100644
--- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
@@ -1,21 +1,23 @@
+- can_resolve = @merge_request.conflicts_can_be_resolved_by?(current_user)
+- can_resolve_in_ui = @merge_request.conflicts_can_be_resolved_in_ui?
+- can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user)
+
%h4.has-conflicts
= icon("exclamation-triangle")
This merge request contains merge conflicts
%p
- Please
- - if @merge_request.conflicts_can_be_resolved_by?(current_user)
- - if @merge_request.conflicts_can_be_resolved_in_ui?
- = link_to "resolve these conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- - else
- %span.has-tooltip{title: "These conflicts cannot be resolved through GitLab"}
- resolve these conflicts locally
- - else
- resolve these conflicts
-
+ To merge this request, resolve these conflicts
+ - if can_resolve && !can_resolve_in_ui
+ locally
or
+ - unless can_merge
+ ask someone with write access to this repository to
+ merge it locally.
- - if @merge_request.can_be_merged_via_command_line_by?(current_user)
- #{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}.
- - else
- ask someone with write access to this repository to merge this request manually.
+- if (can_resolve && can_resolve_in_ui) || can_merge
+ .btn-group
+ - if can_resolve && can_resolve_in_ui
+ = link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn"
+ - if can_merge
+ = link_to "Merge locally", "#modal_merge_info", class: "btn how_to_merge_link vlink", "data-toggle" => "modal"
diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
index 2b6b5e05e86..f70cd09c5f4 100644
--- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
@@ -1,6 +1,9 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
+
%h4
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
- to be merged automatically when the build succeeds.
+ to be merged automatically when the pipeline succeeds.
%div
%p
= succeed '.' do
@@ -21,5 +24,5 @@
Remove Source Branch When Merged
- if user_can_cancel_automatic_merge
- = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do
+ = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do
Cancel Automatic Merge
diff --git a/app/views/projects/merge_requests/widget/open/_nothing.html.haml b/app/views/projects/merge_requests/widget/open/_nothing.html.haml
index 35626b624b7..7af8c01c134 100644
--- a/app/views/projects/merge_requests/widget/open/_nothing.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_nothing.html.haml
@@ -1,4 +1,4 @@
-%h4
+%h4
= icon("exclamation-triangle")
Nothing to merge from
%span.label-branch= source_branch_with_namespace(@merge_request)
diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
index 35d5677ee37..e094f97f3b6 100644
--- a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml
@@ -3,4 +3,8 @@
This merge request has unresolved discussions
%p
- Please resolve these discussions to allow this merge request to be merged. \ No newline at end of file
+ Please resolve these discussions
+ - if @project.issues_enabled? && can?(current_user, :create_issue, @project)
+ or
+ = link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_for_resolving_discussions: @merge_request.iid)
+ to allow this merge request to be merged.
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index cbf1ba04170..0f4a8508751 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -14,17 +14,12 @@
= render 'projects/notes/hints'
.clearfix
.error-alert
- .col-md-6
- .form-group
- = f.label :due_date, "Due Date", class: "control-label"
- .col-sm-10
- = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
- %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
+ = render "shared/milestones/form_dates", f: f
.form-actions
- if @milestone.new_record?
= f.submit 'Create milestone', class: "btn-create btn"
= link_to "Cancel", namespace_project_milestones_path(@project.namespace, @project), class: "btn btn-cancel"
- -else
+ - else
= f.submit 'Save changes', class: "btn-save btn"
= link_to "Cancel", namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-cancel"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index f9ba77e87b5..c3a6096aa54 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -4,21 +4,24 @@
= render "projects/issues/head"
%div{ class: container_class }
- .detail-page-header
+ .detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) }
- if @milestone.closed?
Closed
- elsif @milestone.expired?
Past due
+ - elsif @milestone.upcoming?
+ Upcoming
- else
Open
- %span.identifier
- Milestone ##{@milestone.iid}
- - if @milestone.expires_at
- %span.creator
- &middot;
- = @milestone.expires_at
- .pull-right
+ .header-text-content
+ %span.identifier
+ Milestone ##{@milestone.iid}
+ - if @milestone.due_date || @milestone.start_date
+ %span.creator
+ &middot;
+ = milestone_date_range(@milestone)
+ .milestone-buttons
- if can?(current_user, :admin_milestone, @project)
- if @milestone.active?
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 932603f03b0..064e92b15eb 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -69,10 +69,15 @@
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'Fogbugz')
%div
+ - if gitea_import_enabled?
+ = link_to new_import_gitea_url, class: 'btn import_gitea' do
+ = custom_icon('go_logo')
+ Gitea
+ %div
- if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do
= icon('git', text: 'Repo by URL')
- %div{ class: 'import_gitlab_project' }
+ .import_gitlab_project
- if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
@@ -90,7 +95,8 @@
= f.label :visibility_level, class: 'label-light' do
Visibility Level
= link_to "(?)", help_page_path("public_access/public_access")
- = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project)
+ = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project
+
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 8620f492282..e8e450742b5 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,11 +1,14 @@
.note-edit-form
- = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f|
- = note_target_fields(note)
- = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
+ = form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do
+ = hidden_field_tag :target_id, '', class: 'js-form-target-id'
+ = hidden_field_tag :target_type, '', class: 'js-form-target-type'
+ = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do
+ = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints'
.note-form-actions.clearfix
- = f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button'
+ .settings-message.note-edit-warning.js-edit-warning
+ Finish editing this message first!
+ = submit_tag 'Save Comment', class: 'btn btn-nr btn-save js-comment-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
Cancel
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 46b402545cd..b561052e721 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -3,6 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
+ = hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
= note_target_fields(@note)
= f.hidden_field :commit_id
= f.hidden_field :line_code
@@ -23,5 +24,5 @@
.note-form-actions.clearfix
= f.submit 'Comment', class: "btn btn-nr btn-create append-right-10 comment-btn js-comment-button"
= yield(:note_actions)
- %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
+ %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
Discard draft
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index ab719e38904..36c388c3318 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -5,15 +5,18 @@
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
.timeline-entry-inner
.timeline-icon
- %a{href: user_path(note.author)}
+ %a{ href: user_path(note.author) }
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
= link_to_member(note.project, note.author, avatar: false)
- .inline.note-headline-light
+ .note-headline-light
= note.author.to_reference
- unless note.system
commented
+ - if note.system
+ %span.system-note-message
+ = note.redacted_note_html
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
@@ -32,7 +35,7 @@
"resolved-by" => "#{note.resolved_by.try(:name)}",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
- "v-ref:note_#{note.id}" => true }
+ "ref" => "note_#{note.id}" }
.note-action-button
= icon("spin spinner", "v-show" => "loading")
@@ -43,7 +46,7 @@
"@click" => "resolve",
":title" => "buttonText",
"v-show" => "!loading",
- "v-el:button" => true }
+ ":ref" => "'button'" }
= render "shared/icons/icon_status_success.svg"
@@ -58,16 +61,20 @@
= icon('pencil', class: 'link-highlight')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
- .note-body{class: note_editable ? 'js-task-list-container' : ''}
+ .note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
= preserve do
= note.redacted_note_html
- = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
+ = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
- = render 'projects/notes/edit_form', note: note
+ .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
+ #{note.note}
+ %textarea.hidden.js-task-list-field.original-task-list #{note.note}
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
-
+ - if note.system
+ .system-note-commit-list-toggler
+ Toggle commit list
- if note.attachment.url
.note-attachment
- if note.attachment.image?
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 00b62a595ff..fbd2bff5bbb 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -1,5 +1,8 @@
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
+
+= render 'projects/notes/edit_form'
+
%ul.notes.notes-form.timeline
%li.timeline-entry
.flash-container.timeline-content
@@ -20,4 +23,4 @@
to post a comment
:javascript
- var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
+ var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/app/views/projects/pipelines/_graph.html.haml b/app/views/projects/pipelines/_graph.html.haml
new file mode 100644
index 00000000000..0202833c0bf
--- /dev/null
+++ b/app/views/projects/pipelines/_graph.html.haml
@@ -0,0 +1,4 @@
+- pipeline = local_assigns.fetch(:pipeline)
+.pipeline-visualization.pipeline-graph
+ %ul.stage-column-list
+ = render partial: "projects/stage/graph", collection: pipeline.stages, as: :stage
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index d288efc546f..ca76f13ef5e 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,39 +1,47 @@
-%p
-.commit-info-row
- Pipeline
- = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace"
- with
- = pluralize @pipeline.statuses.count(:id), "build"
- - if @pipeline.ref
- for
- = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- - if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
- - if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
-
- .pull-right
- = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
- = ci_icon_for_status(@pipeline.status)
- = ci_label_for_status(@pipeline.status)
-
-- if @commit
- .commit-info-row
- %span.light Authored by
- %strong
- = commit_author_link(@commit, avatar: true, size: 24)
- #{time_ago_with_tooltip(@commit.authored_date)}
-
-.commit-info-row
- %span.light Commit
- = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace"
- = clipboard_button(clipboard_text: @pipeline.sha)
+.page-content-header
+ .header-main-content
+ = render 'ci/status/badge', status: @pipeline.detailed_status(current_user)
+ %strong Pipeline ##{@commit.pipelines.last.id}
+ triggered #{time_ago_with_tooltip(@commit.authored_date)} by
+ = author_avatar(@commit, size: 24)
+ = commit_author_link(@commit)
+ .header-action-buttons
+ - if can?(current_user, :update_pipeline, @pipeline.project)
+ - if @pipeline.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post
+ - if @pipeline.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- if @commit
- .commit-box.content-block
+ .commit-box
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present?
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line))
+
+.info-well
+ - if @commit.status
+ .well-segment.pipeline-info
+ %div{ class: "icon-container ci-status-icon-#{@commit.status}" }
+ = ci_icon_for_status(@commit.status)
+ = pluralize @pipeline.statuses.count(:id), "build"
+ - if @pipeline.ref
+ from
+ = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
+ - if @pipeline.duration
+ in
+ = time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+
+ .well-segment.branch-info
+ .icon-container.commit-icon
+ = custom_icon("icon_commit")
+ = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace js-details-short"
+ = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
+ %span.text-expander
+ \...
+ %span.js-details-content.hide
+ = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace commit-hash-full"
+ = clipboard_button(clipboard_text: @pipeline.sha, title: "Copy commit SHA to clipboard")
diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml
new file mode 100644
index 00000000000..a0b14a7274a
--- /dev/null
+++ b/app/views/projects/pipelines/_stage.html.haml
@@ -0,0 +1,3 @@
+- @stage.statuses.latest.each do |status|
+ %li
+ = render 'ci/status/dropdown_graph_badge', subject: status
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
new file mode 100644
index 00000000000..88af41aa835
--- /dev/null
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -0,0 +1,42 @@
+.tabs-holder
+ %ul.pipelines-tabs.nav-links.no-top.no-bottom
+ %li.js-pipeline-tab-link
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
+ Pipeline
+ %li.js-builds-tab-link
+ = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
+ Builds
+ %span.badge.js-builds-counter= pipeline.statuses.count
+
+
+
+.tab-content
+ #js-tab-pipeline.tab-pane
+ .build-content.middle-block.js-pipeline-graph
+ = render "projects/pipelines/graph", pipeline: pipeline
+
+ #js-tab-builds.tab-pane
+ - if pipeline.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - pipeline.yaml_errors.split(",").each do |error|
+ %li= error
+ You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
+
+ - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+ .table-holder.pipeline-holder
+ %table.table.ci-table.pipeline
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Name
+ %th
+ - if pipeline.project.build_coverage_enabled?
+ %th Coverage
+ %th
+ = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 4bc49072f35..df36279ed75 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -5,23 +5,23 @@
%div{ class: container_class }
.top-area
%ul.nav-links
- %li{class: ('active' if @scope.nil?)}
+ %li{ class: ('active' if @scope.nil?) }>
= link_to project_pipelines_path(@project) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(@pipelines_count)
- %li{class: ('active' if @scope == 'running')}
+ %li{ class: ('active' if @scope == 'running') }>
= link_to project_pipelines_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@running_or_pending_count)
- %li{class: ('active' if @scope == 'branches')}
+ %li{ class: ('active' if @scope == 'branches') }>
= link_to project_pipelines_path(@project, scope: :branches) do
Branches
- %li{class: ('active' if @scope == 'tags')}
+ %li{ class: ('active' if @scope == 'tags') }>
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
@@ -35,22 +35,33 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
-
- %div.content-list.pipelines
- - stages = @pipelines.stages
+ .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } }
- if @pipelines.blank?
%div
.nothing-here-block No pipelines to show
- else
- .table-holder
- %table.table.ci-table
- %thead
- %th Status
- %th Pipeline
- %th Commit
- %th Stages
- %th
- %th.hidden-xs
- = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
-
- = paginate @pipelines, theme: 'gitlab'
+ .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
+ "icon_status_canceled" => custom_icon("icon_status_canceled"),
+ "icon_status_running" => custom_icon("icon_status_running"),
+ "icon_status_skipped" => custom_icon("icon_status_skipped"),
+ "icon_status_created" => custom_icon("icon_status_created"),
+ "icon_status_pending" => custom_icon("icon_status_pending"),
+ "icon_status_success" => custom_icon("icon_status_success"),
+ "icon_status_failed" => custom_icon("icon_status_failed"),
+ "icon_status_warning" => custom_icon("icon_status_warning"),
+ "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
+ "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
+ "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
+ "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
+ "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
+ "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
+ "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
+ "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
+ "icon_play" => custom_icon("icon_play"),
+ "icon_timer" => custom_icon("icon_timer"),
+ "icon_status_manual" => custom_icon("icon_status_manual"),
+ } }
+
+ .vue-pipelines-index
+
+= page_specific_javascript_tag('vue_pipelines_index/index.js')
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 688535ad764..49c1d886423 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -2,10 +2,8 @@
- page_title "Pipeline"
= render "projects/pipelines/head"
-%div{ class: container_class }
- .prepend-top-default
- - if @commit
- = render "projects/pipelines/info"
- %div.block-connector
+.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } }
+ - if @commit
+ = render "projects/pipelines/info"
- = render "projects/commit/pipeline", pipeline: @pipeline
+ = render "projects/pipelines/with_tabs", pipeline: @pipeline
diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml
index 7b7fa56d993..22a3b884520 100644
--- a/app/views/projects/pipelines_settings/_badge.html.haml
+++ b/app/views/projects/pipelines_settings/_badge.html.haml
@@ -1,4 +1,4 @@
-.row{ class: badge.title.gsub(' ', '-') }
+%div{ class: badge.title.gsub(' ', '-') }
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
= badge.title.capitalize
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
index 96221a20502..1f698558bce 100644
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ b/app/views/projects/pipelines_settings/show.html.haml
@@ -86,6 +86,9 @@
%li
tap --coverage-report=text-summary (NodeJS) -
%code ^Statements\s*:\s*([^%]+)
+ %li
+ excoveralls (Elixir) -
+ %code \[TOTAL\]\s+(\d+\.\d+)%
= f.submit 'Save changes', class: "btn btn-save"
diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml
new file mode 100644
index 00000000000..ab0771b5751
--- /dev/null
+++ b/app/views/projects/project_members/_index.html.haml
@@ -0,0 +1,22 @@
+.row.prepend-top-default
+ .col-lg-3.settings-sidebar
+ %h4.prepend-top-0
+ Members
+ - if can?(current_user, :admin_project_member, @project)
+ %p
+ Add a new member to
+ %strong= @project.name
+ .col-lg-9
+ .light.prepend-top-default
+ - if can?(current_user, :admin_project_member, @project)
+ = render "projects/project_members/new_project_member"
+
+ = render 'shared/members/requests', membership_source: @project, requesters: @requesters
+ .append-bottom-default.clearfix
+ %h5.member.existing-title
+ Existing members and groups
+ - if @group_links.any?
+ = render 'projects/project_members/groups', group_links: @group_links
+
+ = render 'projects/project_members/team', members: @project_members
+ = paginate @project_members, theme: "gitlab"
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 79dcd7a6ee9..2b1c23f7dda 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -1,22 +1,18 @@
= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
- .row
- .col-md-4.col-lg-6
- = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true)
- .help-block.append-bottom-10
- Search for users by name, username, or email, or invite new ones using their email address.
-
- .col-md-3.col-lg-2
- = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
- .help-block.append-bottom-10
- = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
- about role permissions
-
- .col-md-3.col-lg-2
- .clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
- %i.clear-icon.js-clear-input
- .help-block.append-bottom-10
- On this date, the user(s) will automatically lose access to this project.
-
- .col-md-2
- = f.submit "Add to project", class: "btn btn-create btn-block"
+ .form-group
+ = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
+ .help-block.append-bottom-10
+ Search for members by name, username, or email, or invite new ones using their email address.
+ .form-group
+ = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
+ .help-block.append-bottom-10
+ = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+ about role permissions
+ .form-group
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block.append-bottom-10
+ On this date, the member(s) will automatically lose access to this project.
+ = f.submit "Add to project", class: "btn btn-create"
+ = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project"
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index c1e894d8f40..5292e73be7a 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,7 +1,13 @@
.panel.panel-default
.panel-heading
- Users with access to
+ Members with access to
%strong #{@project.name}
%span.badge= @project_members.total_count
+ = form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
+ = render 'shared/members/sort_dropdown'
%ul.content-list
= render partial: 'shared/members/member', collection: members, as: :member
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index eef97107d77..42ce4f8001b 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -12,5 +12,4 @@
.form-actions
= button_tag 'Import project members', class: "btn btn-create"
- = link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel"
-
+ = link_to "Cancel", namespace_project_settings_members_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
deleted file mode 100644
index bdeb704b6da..00000000000
--- a/app/views/projects/project_members/index.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-- page_title "Members"
-
-.project-members-page.prepend-top-default
- %h4.project-members-title.clearfix
- Members
- = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project"
- - if can?(current_user, :admin_project_member, @project)
- .project-members-new.append-bottom-default
- %p.clearfix
- Add new user to
- %strong= @project.name
- = render "new_project_member"
-
- = render 'shared/members/requests', membership_source: @project, requesters: @requesters
-
- .append-bottom-default.clearfix
- %h5.member.existing-title
- Existing users and groups
- = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
- - if @group_links.any?
- = render 'groups', group_links: @group_links
-
- = render 'team', members: @project_members
- = paginate @project_members, theme: "gitlab"
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 91927181efb..d15f4310ff5 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,3 +1,4 @@
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
$("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index 44fa4b60343..d07bb661615 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -14,8 +14,8 @@
// Load more commit logs for each file in tree
// if we still on the same page
var url = "#{escape_javascript(@more_log_url)}";
- ajaxGet(url);
+ gl.utils.ajaxGet(url);
}
:plain
- gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody')); \ No newline at end of file
+ gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody'));
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 6e58e5a0c78..7036b8a5ccc 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -1,4 +1,4 @@
-%li.runner{id: dom_id(runner)}
+%li.runner{ id: dom_id(runner) }
%h4
= runner_status_icon(runner)
%span.monospace
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index b41edeb2c7e..fc338dcf887 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -7,14 +7,14 @@
%p= @service.description
.col-lg-9
= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
- = render 'shared/service_settings', form: form
+ = render 'shared/service_settings', form: form, subject: @service
+ .footer-block.row-content-block
+ = form.submit 'Save changes', class: 'btn btn-save'
+ &nbsp;
+ - if @service.valid? && @service.activated?
+ - unless @service.can_test?
+ - disabled_class = 'disabled'
+ - disabled_title = @service.disabled_title
- = form.submit 'Save changes', class: 'btn btn-save'
- &nbsp;
- - if @service.valid? && @service.activated?
- - unless @service.can_test?
- - disabled_class = 'disabled'
- - disabled_title = @service.disabled_title
-
- = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title
- = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel"
+ = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title
+ = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/_index.html.haml
index 4a33a5bc6f6..964133504e6 100644
--- a/app/views/projects/services/index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -1,5 +1,3 @@
-- page_title "Services"
-
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
@@ -28,5 +26,6 @@
%td.hidden-xs
= service.description
%td.light
- = time_ago_in_words service.updated_at
- ago
+ - if service.updated_at.present?
+ = time_ago_in_words service.updated_at
+ ago
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
new file mode 100644
index 00000000000..8ca4c51a064
--- /dev/null
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -0,0 +1,91 @@
+- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+
+To setup this service:
+%ul.list-unstyled
+ %li
+ 1.
+ = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
+ on your Mattermost installation
+ %li
+ 2.
+ = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
+ in Mattermost with these options:
+
+%hr
+
+.help-form
+ .form-group
+ = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#display_name')
+
+ .form-group
+ = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#description')
+
+ .form-group
+ = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+
+ .form-group
+ = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#request_url')
+
+ .form-group
+ = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block POST
+
+ .form-group
+ = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#response_username')
+
+ .form-group
+ = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#response_icon')
+
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block Yes
+
+ .form-group
+ = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_hint')
+
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_description')
+
+%hr
+
+%ul.list-unstyled
+ %li
+ 3. After adding the slash command, paste the
+
+ %strong token
+ into the field below
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
new file mode 100644
index 00000000000..c1e576b42fc
--- /dev/null
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -0,0 +1,15 @@
+- enabled = Gitlab.config.mattermost.enabled
+
+.well
+ This service allows GitLab users to perform common operations on this
+ project by entering slash commands in Mattermost.
+ %br
+ See list of available commands in Mattermost after setting up this service,
+ by entering
+ %code /&lt;command_trigger_word&gt; help
+
+ - unless enabled || @service.template?
+ = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
+
+- if enabled && !@service.template?
+ = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
new file mode 100644
index 00000000000..c929eee3bb9
--- /dev/null
+++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
@@ -0,0 +1,7 @@
+.services-installation-info
+ - unless @service.activated?
+ .row
+ .col-sm-9.col-sm-offset-3
+ = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do
+ = custom_icon('mattermost_logo', size: 15)
+ = 'Add to Mattermost'
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
new file mode 100644
index 00000000000..04b9100acc6
--- /dev/null
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -0,0 +1,95 @@
+- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path"
+- run_actions_text = "Perform common operations on this project: #{pretty_name}"
+
+.well
+ This service allows GitLab users to perform common operations on this
+ project by entering slash commands in Slack.
+ %br
+ See list of available commands in Slack after setting up this service,
+ by entering
+ %code /&lt;command&gt; help
+ %br
+ %br
+ - unless @service.template?
+ To setup this service:
+ %ul.list-unstyled
+ %li
+ 1.
+ = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
+ in your Slack team with these options:
+
+ %hr
+
+ .help-form
+ .form-group
+ = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+
+ .form-group
+ = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#url')
+
+ .form-group
+ = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block POST
+
+ .form-group
+ = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#customize_name')
+
+ .form-group
+ = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
+ = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
+
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list
+
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_description')
+
+ .form-group
+ = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_usage_hint')
+
+ .form-group
+ = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#descriptive_label')
+
+ %hr
+
+ %ul.list-unstyled
+ %li
+ 2. Paste the
+ %strong Token
+ into the field below
+ %li
+ 3. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Slack!
diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index ceabe2eab3d..ceabe2eab3d 100644
--- a/app/views/projects/hooks/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
new file mode 100644
index 00000000000..aa38a889cdd
--- /dev/null
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -0,0 +1,3 @@
+- page_title 'Integrations'
+= render 'projects/hooks/index'
+= render 'projects/services/index'
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
new file mode 100644
index 00000000000..d81ed7bb609
--- /dev/null
+++ b/app/views/projects/settings/members/show.html.haml
@@ -0,0 +1,6 @@
+- page_title "Members"
+
+= render "projects/project_members/index"
+- if can?(current_user, :admin_project, @project)
+ - if @project.allowed_to_share_with_group?
+ = render "projects/group_links/index"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 4de95036eef..80d4081dd7b 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -17,10 +17,10 @@
%ul.nav
%li
= link_to project_files_path(@project) do
- Files (#{repository_size})
+ Files (#{storage_counter(@project.statistics.total_repository_size)})
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
- #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
+ #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)})
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
@@ -64,19 +64,15 @@
- unless @repository.gitlab_ci_yml
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
- Set Up CI
-
- %li.project-repo-buttons-right
- .project-repo-buttons.project-right-buttons
- - if current_user
- = render 'shared/members/access_request_buttons', source: @project
- = render "projects/buttons/koding"
-
- .btn-group.project-repo-btn-group
- = render 'projects/buttons/download', project: @project, ref: @ref
- = render 'projects/buttons/dropdown'
+ Set up CI
+ - if koding_enabled? && @repository.koding_yml.blank?
+ %li.missing
+ = link_to 'Set up Koding', add_koding_stack_path(@project)
+ - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
+ Set up auto deploy
- = render 'shared/notifications/button', notification_setting: @notification_setting
- if @repository.commit
.project-last-commit{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 32e1f8a21b0..068a6610350 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
- - if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do
- New snippet
- - if can?(current_user, :update_project_snippet, @snippet)
- = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
- Delete
- if can?(current_user, :update_project_snippet, @snippet)
- = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
+ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do
Edit
+ - if can?(current_user, :update_project_snippet, @snippet)
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
+ Delete
+ - if can?(current_user, :create_project_snippet, @project)
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
+ New snippet
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index e77e1b026f6..84e05cd6d88 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,11 +1,19 @@
- page_title "Snippets"
-.sub-header-block
- - if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
- New snippet
+- if current_user
+ .top-area
+ - include_private = @project.team.member?(current_user) || current_user.admin?
+ = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
+
+ .nav-controls.hidden-xs
+ - if can?(current_user, :create_project_snippet, @project)
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do
+ New snippet
- .oneline
- Share code pastes with others out of git repository
+- if can?(current_user, :create_project_snippet, @project)
+ .visible-xs
+ &nbsp;
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do
+ New snippet
= render 'snippets/snippets'
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 9503dbded13..485b23815bc 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -8,10 +8,11 @@
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
= link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
= render 'shared/snippets/blob'
- = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+ .row-content-block.top-block.content-component-block
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
- %div#notes= render "projects/notes/notes_with_form"
+ #notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml
new file mode 100644
index 00000000000..4ee30b023ac
--- /dev/null
+++ b/app/views/projects/stage/_graph.html.haml
@@ -0,0 +1,19 @@
+- stage = local_assigns.fetch(:stage)
+- statuses = stage.statuses.latest
+- status_groups = statuses.sort_by(&:sortable_name).group_by(&:group_name)
+%li.stage-column
+ .stage-name
+ %a{ name: stage.name }
+ = stage.name.titleize
+ .builds-container
+ %ul
+ - status_groups.each do |group_name, grouped_statuses|
+ - if grouped_statuses.one?
+ - status = grouped_statuses.first
+ %li.build{ 'id' => "ci-badge-#{group_name}" }
+ .curve
+ = render 'ci/status/graph_badge', subject: status
+ - else
+ %li.build{ 'id' => "ci-badge-#{group_name}" }
+ .curve
+ = render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml
new file mode 100644
index 00000000000..9c5eb501174
--- /dev/null
+++ b/app/views/projects/stage/_in_stage_group.html.haml
@@ -0,0 +1,14 @@
+- group_status = CommitStatus.where(id: subject).status
+%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
+ %span{ class: "ci-status-icon ci-status-icon-#{group_status}" }
+ = ci_icon_for_status(group_status)
+ %span.ci-status-text
+ = name
+ %span.dropdown-counter-badge= subject.size
+
+%ul.dropdown-menu.big-pipeline-graph-dropdown-menu.js-grouped-pipeline-dropdown
+ .arrow
+ .scrollable-menu
+ - subject.each do |status|
+ %li
+ = render 'ci/status/dropdown_graph_badge', subject: status
diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml
new file mode 100644
index 00000000000..28e1c060875
--- /dev/null
+++ b/app/views/projects/stage/_stage.html.haml
@@ -0,0 +1,13 @@
+%tr
+ %th{ colspan: 10 }
+ %strong
+ %a{ name: stage.name }
+ %span{ class: "ci-status-link ci-status-icon-#{stage.status}" }
+ = ci_icon_for_status(stage.status)
+ &nbsp;
+ = stage.name.titleize
+= render stage.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true
+= render stage.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true
+%tr
+ %td{ colspan: 10 }
+ &nbsp;
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index c42641afea0..8ef069b9e05 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -1,7 +1,7 @@
- commit = @repository.commit(tag.dereferenced_target)
- release = @releases.find { |release| release.tag == tag.name }
-%li
- %div
+%li.flex-row
+ .row-main-content.str-truncated
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
%span.item-title
= icon('tag')
@@ -10,24 +10,25 @@
&nbsp;
= strip_gpg_signature(tag.message)
- .controls
- = render 'projects/buttons/download', project: @project, ref: tag.name
+ - if commit
+ .block-truncated
+ = render 'projects/branches/commit', commit: commit, project: @project
+ - else
+ %p
+ Cant find HEAD commit for this tag
+ - if release && release.description.present?
+ .description.prepend-top-default
+ .wiki
+ = preserve do
+ = markdown_field(release, :description)
- - if can?(current_user, :push_code, @project)
- = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
- = icon("pencil")
+ .row-fixed-content.controls
+ = render 'projects/buttons/download', project: @project, ref: tag.name
- - if can?(current_user, :admin_project, @project)
- = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
- = icon("trash-o")
+ - if can?(current_user, :push_code, @project)
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
+ = icon("pencil")
- - if commit
- = render 'projects/branches/commit', commit: commit, project: @project
- - else
- %p
- Cant find HEAD commit for this tag
- - if release && release.description.present?
- .description.prepend-top-default
- .wiki
- = preserve do
- = markdown_field(release, :description)
+ - if can?(current_user, :admin_project, @project)
+ = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = icon("trash-o")
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 7a0d9dcc94f..e2f132f7742 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -2,23 +2,24 @@
- page_title "Tags"
= render "projects/commits/head"
-%div{ class: container_class }
- .top-area
- .nav-text
+.flex-list{ class: container_class }
+ .top-area.flex-row
+ .nav-text.row-main-content
Tags give the ability to mark specific points in history as being important
- .nav-controls
+ .nav-controls.row-fixed-content
= form_tag(filter_tags_path, method: :get) do
= search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
- .dropdown.inline
- %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
+
+ .dropdown
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} }
%span.light
- = @sort.humanize
- = icon('caret-down')
+ = projects_sort_options_hash[@sort]
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
- = link_to filter_tags_path(sort: nil) do
- Name
+ = link_to filter_tags_path(sort: sort_value_name) do
+ = sort_title_name
= link_to filter_tags_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated
= link_to filter_tags_path(sort: sort_value_oldest_updated) do
@@ -29,7 +30,7 @@
.tags
- if @tags.any?
- %ul.content-list
+ %ul.flex-list.content-list
= render partial: 'tag', collection: @tags
= paginate @tags, theme: 'gitlab'
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 3a097750d6e..160d4c7a223 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -2,7 +2,7 @@
- if @error
.alert.alert-danger
- %button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
+ %button.close{ type: "button", "data-dismiss" => "alert" } &times;
= @error
%h3.page-title
@@ -23,7 +23,7 @@
= label_tag :message, nil, class: 'control-label'
.col-sm-10
= text_area_tag :message, nil, required: false, tabindex: 3, class: 'form-control', rows: 5
- .help-block Optionally, enter a message to create an annotated tag.
+ .help-block Optionally, add a message to the tag.
%hr
.form-group
= label_tag :release_description, 'Release notes', class: 'control-label'
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 155af755759..fad3c5c2173 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -3,29 +3,32 @@
= render "projects/commits/head"
%div{ class: container_class }
- .sub-header-block
- .pull-right.tag-buttons
- - if can?(current_user, :push_code, @project)
- = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Edit release notes' do
- = icon("pencil")
- = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse files' do
- = icon('files-o')
- = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do
- = icon('history')
- = render 'projects/buttons/download', project: @project, ref: @tag.name
- - if can?(current_user, :admin_project, @project)
- .pull-right
- = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
- %i.fa.fa-trash-o
- .tag-info.append-bottom-10
+ .top-area.multi-line
+ .nav-text
.title
%span.item-title= @tag.name
- if @commit
= render 'projects/branches/commit', commit: @commit, project: @project
- else
Cant find HEAD commit for this tag
+
+ .nav-controls.controls-flex
+ - if can?(current_user, :push_code, @project)
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Edit release notes' do
+ = icon("pencil")
+ = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse files' do
+ = icon('files-o')
+ = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse commits' do
+ = icon('history')
+ .btn-container.controls-item
+ = render 'projects/buttons/download', project: @project, ref: @tag.name
+ - if can?(current_user, :admin_project, @project)
+ .btn-container.controls-item-full
+ = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
+ %i.fa.fa-trash-o
+
- if @tag.message.present?
- %pre.body
+ %pre.wrap
= strip_gpg_signature(@tag.message)
.append-bottom-default.prepend-top-default
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index ee417b58cbf..425b460eb09 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -6,4 +6,4 @@
%span.str-truncated= file_name
%td.hidden-xs.tree-commit
%td.tree-time-ago.cgray.text-right
- = render 'projects/tree/spinner' \ No newline at end of file
+ = render 'projects/tree/spinner'
diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml
index 2b5f671c09e..04d52361db0 100644
--- a/app/views/projects/tree/_submodule_item.html.haml
+++ b/app/views/projects/tree/_submodule_item.html.haml
@@ -1,4 +1,4 @@
-%tr{ class: "tree-item" }
+%tr.tree-item
%td.tree-item-file-name
%i.fa.fa-archive.fa-fw
= submodule_link(submodule_item, @ref)
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 21e378b8735..2c08221565b 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,18 +1,15 @@
-%div.tree-content-holder
+.tree-content-holder
.table-holder
- %table.table#tree-slider{class: "table_#{@hex_path} tree-table" }
+ %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
%thead
%tr
%th Name
%th.hidden-xs
- .pull-left Last Commit
+ .pull-left Last commit
.last-commit.hidden-sm.pull-left
- &nbsp;
- %i.fa.fa-angle-right
- &nbsp;
%small.light
+ = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard")
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
- &ndash;
= time_ago_with_tooltip(@commit.committed_date)
= @commit.full_title
%small.commit-history-link-spacer &#124;
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 1c5f8b3928b..259207a6dfd 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -15,11 +15,11 @@
- if current_user
%li
- if !on_top_of_branch?
- %span.btn.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }}
+ %span.btn.add-to-tree.disabled.has-tooltip{ title: "You can only add files when you are on a branch", data: { container: 'body' } }
= icon('plus')
- else
%span.dropdown
- %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"}
+ %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown" }
= icon('plus')
%ul.dropdown-menu
- if can_edit_tree?
@@ -28,11 +28,11 @@
= icon('pencil fw')
New file
%li
- = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do
+ = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
= icon('file fw')
Upload file
%li
- = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do
+ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
= icon('folder fw')
New directory
- elsif can?(current_user, :fork_project, @project)
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index 1ccef6d52ab..15c9536133c 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -6,4 +6,4 @@
%span.str-truncated= path
%td.hidden-xs.tree-commit
%td.tree-time-ago.text-right
- = render 'projects/tree/spinner' \ No newline at end of file
+ = render 'projects/tree/spinner'
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index f6e0b0a7c8a..6e5dd1b196d 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -76,6 +76,16 @@
script:
- "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
%h5.prepend-top-default
+ Use webhook
+
+ %p.light
+ Add the following webhook to another project for Push and Tag push events.
+ The project will be rebuilt at the corresponding event.
+
+ %pre
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN
+ %h5.prepend-top-default
Pass build variables
%p.light
@@ -83,10 +93,18 @@
%code variables[VARIABLE]=VALUE
to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
- %pre.append-bottom-0
+ With cURL:
+
+ %pre
:plain
curl -X POST \
-F token=TOKEN \
-F "ref=REF_NAME" \
-F "variables[RUN_NIGHTLY_BUILD]=true" \
#{builds_trigger_url(@project.id)}
+ %p.light
+ With webhook:
+
+ %pre.append-bottom-0
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN&variables[RUN_NIGHTLY_BUILD]=true
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml
index 07cee86ba4c..c7cebf45160 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/projects/variables/_table.html.haml
@@ -12,8 +12,8 @@
- @project.variables.order_key_asc.each do |variable|
- if variable.id?
%tr
- %td= variable.key
- %td= variable.value
+ %td.variable-key= variable.key
+ %td.variable-value{ "data-value" => variable.value }******
%td
= link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do
%span.sr-only
diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml
index 09bb54600af..cf7ae0b489f 100644
--- a/app/views/projects/variables/index.html.haml
+++ b/app/views/projects/variables/index.html.haml
@@ -15,3 +15,4 @@
No variables found, add one with the form above.
- else
= render "table"
+ %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 4e41a15d9f4..c52527332bc 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -34,9 +34,6 @@
- if @page && @page.persisted?
= f.submit 'Save changes', class: "btn-save btn"
.pull-right
- - if can?(current_user, :admin_wiki, @project)
- = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do
- Delete
= link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped"
- else
= f.submit 'Create page', class: "btn-create btn"
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
deleted file mode 100644
index 09c4411d67e..00000000000
--- a/app/views/projects/wikis/_nav.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
- = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
-
- = nav_link(path: 'wikis#pages') do
- = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
-
- = nav_link(path: 'wikis#git_access') do
- = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
- Git Access
-
- = render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index c32cb122c26..c74f53b4c39 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -1,11 +1,11 @@
- @no_container = true
%div{ class: container_class }
- %div#modal-new-wiki.modal
+ #modal-new-wiki.modal
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title New Wiki Page
.modal-body
%form.new-wiki-page
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
new file mode 100644
index 00000000000..cad9c15a49e
--- /dev/null
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -0,0 +1,23 @@
+%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar
+ .block.wiki-sidebar-header.append-bottom-default
+ %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
+ = icon('angle-double-right')
+
+ - git_access_url = namespace_project_wikis_git_access_path(@project.namespace, @project)
+ = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do
+ = succeed '&nbsp;' do
+ = icon('cloud-download')
+ Clone repository
+
+ .blocks-container
+ .block.block-first
+ %ul.wiki-pages
+ - @sidebar_wiki_pages.each do |wiki_page|
+ %li{ class: params[:id] == wiki_page.slug ? 'active' : '' }
+ = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do
+ = wiki_page.title.capitalize
+ .block
+ = link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do
+ More Pages
+
+= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 679d6018bef..8cf018da1b7 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,23 +1,35 @@
- @no_container = true
- page_title "Edit", @page.title.capitalize, "Wiki"
-= render 'nav'
%div{ class: container_class }
- .top-area
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
.nav-text
- %strong
+ %h2.wiki-page-title
- if @page.persisted?
= link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
- else
= @page.title.capitalize
- %span.light
- &middot;
- Edit Page
+ %span.light
+ &middot;
+ - if @page.persisted?
+ Edit Page
+ - else
+ Create Page
.nav-controls
- - if !(@page && @page.persisted?)
- - if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- New Page
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ New Page
+ - if @page.persisted?
+ = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
+ Page History
+ - if can?(current_user, :admin_wiki, @project)
+ = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do
+ Delete
= render 'form'
+
+= render 'sidebar'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index b8811a28dd6..e25d6a48573 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,34 +1,36 @@
- @no_container = true
- page_title "Git Access", "Wiki"
-= render 'nav'
%div{ class: container_class }
- .sub-header-block
- %span.oneline
- Git access for
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
+ .git-access-header
+ Clone repository
%strong= @project_wiki.path_with_namespace
- .pull-right
- = render "shared/clone_panel", project: @project_wiki
+ = render "shared/clone_panel", project: @project_wiki
+
+ .wiki-git-access
+ %h3 Install Gollum
+ %pre.dark
+ :preserve
+ gem install gollum
- .prepend-top-default
- %fieldset
- %legend Install Gollum:
- %pre.dark
- :preserve
- gem install gollum
+ %h3 Clone your wiki
+ %pre.dark
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
+ cd #{h @project_wiki.path}
- %legend Clone Your Wiki:
- %pre.dark
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
- cd #{h @project_wiki.path}
+ %h3 Start Gollum and edit locally
+ %pre.dark
+ :preserve
+ gollum
+ == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
+ >> Thin web server (v1.5.0 codename Knife)
+ >> Maximum connections set to 1024
+ >> Listening on 0.0.0.0:4567, CTRL+C to stop
- %legend Start Gollum And Edit Locally:
- %pre.dark
- :preserve
- gollum
- == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
- >> Thin web server (v1.5.0 codename Knife)
- >> Maximum connections set to 1024
- >> Listening on 0.0.0.0:4567, CTRL+C to stop
+= render 'sidebar'
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 4c0b14e2c42..dd7213622c1 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,13 +1,16 @@
- page_title "History", @page.title.capitalize, "Wiki"
-= render 'nav'
+
%div{ class: container_class }
- .top-area
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
.nav-text
- %strong
+ %h2.wiki-page-title
= link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
- %span.light
- &middot;
- History
+ %span.light
+ &middot;
+ History
.table-holder
%table.table
@@ -35,3 +38,5 @@
%td
%strong
= @page.page.wiki.page(@page.page.name, commit.id).try(:format)
+
+= render 'sidebar'
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 9c10acd4cb6..e1eaffc6884 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,9 +1,18 @@
- @no_container = true
- page_title "Pages", "Wiki"
-= render 'nav'
-
%div{ class: container_class }
+ .wiki-page-header
+
+ .nav-text
+ %h2.wiki-page-title
+ Wiki Pages
+
+ .nav-controls
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project), class: 'btn' do
+ = icon('cloud-download')
+ Clone repository
+
%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 5cebb538cf5..1b6dceee241 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,15 +1,19 @@
- @no_container = true
- page_title @page.title.capitalize, "Wiki"
-= render 'nav'
%div{ class: container_class }
- .top-area
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
.nav-text
- %strong= @page.title.capitalize
+ %h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by
- &middot;
- last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ Last edited by
+ %strong
+ #{@page.commit.author.name}
+ #{time_ago_with_tooltip(@page.commit.authored_date)}
.nav-controls
= render 'main_links'
@@ -19,8 +23,9 @@
This is an old version of this page.
You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
-
.wiki-holder.prepend-top-default.append-bottom-default
.wiki
= preserve do
= render_wiki_content(@page)
+
+= render 'sidebar'
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
index a585147ddd1..94e5a5d9709 100644
--- a/app/views/repository_check_mailer/notify.html.haml
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -2,7 +2,7 @@
#{@message}.
%p
- = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1)
+ = link_to "See the affected projects in the GitLab admin panel", admin_projects_url(last_repository_check_failed: 1)
%p
You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml
index 93db151329e..0902c50d052 100644
--- a/app/views/repository_check_mailer/notify.text.haml
+++ b/app/views/repository_check_mailer/notify.text.haml
@@ -1,6 +1,6 @@
#{@message}.
\
-View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)}
+View details: #{admin_projects_url(last_repository_check_failed: 1)}
You are receiving this message because you are a GitLab administrator
for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 2c378231237..8cbecb725b5 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,70 +1,70 @@
%ul.nav-links.search-filter
- if @project
- %li{class: ("active" if @scope == 'blobs')}
+ %li{ class: ("active" if @scope == 'blobs') }
= link_to search_filter_path(scope: 'blobs') do
Code
%span.badge
= @search_results.blobs_count
- %li{class: ("active" if @scope == 'issues')}
+ %li{ class: ("active" if @scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
%span.badge
= @search_results.issues_count
- %li{class: ("active" if @scope == 'merge_requests')}
+ %li{ class: ("active" if @scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
%span.badge
= @search_results.merge_requests_count
- %li{class: ("active" if @scope == 'milestones')}
+ %li{ class: ("active" if @scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
%span.badge
= @search_results.milestones_count
- %li{class: ("active" if @scope == 'notes')}
+ %li{ class: ("active" if @scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do
Comments
%span.badge
= @search_results.notes_count
- %li{class: ("active" if @scope == 'wiki_blobs')}
+ %li{ class: ("active" if @scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
Wiki
%span.badge
= @search_results.wiki_blobs_count
- %li{class: ("active" if @scope == 'commits')}
+ %li{ class: ("active" if @scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do
Commits
%span.badge
= @search_results.commits_count
- elsif @show_snippets
- %li{class: ("active" if @scope == 'snippet_blobs')}
+ %li{ class: ("active" if @scope == 'snippet_blobs') }
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
Snippet Contents
%span.badge
= @search_results.snippet_blobs_count
- %li{class: ("active" if @scope == 'snippet_titles')}
+ %li{ class: ("active" if @scope == 'snippet_titles') }
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
Titles and Filenames
%span.badge
= @search_results.snippet_titles_count
- else
- %li{class: ("active" if @scope == 'projects')}
+ %li{ class: ("active" if @scope == 'projects') }
= link_to search_filter_path(scope: 'projects') do
Projects
%span.badge
= @search_results.projects_count
- %li{class: ("active" if @scope == 'issues')}
+ %li{ class: ("active" if @scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
%span.badge
= @search_results.issues_count
- %li{class: ("active" if @scope == 'merge_requests')}
+ %li{ class: ("active" if @scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
%span.badge
= @search_results.merge_requests_count
- %li{class: ("active" if @scope == 'milestones')}
+ %li{ class: ("active" if @scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
%span.badge
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index ef1c0296d49..938be20c7cf 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -3,7 +3,7 @@
- if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id]
.dropdown
- %button.dropdown-menu-toggle.btn.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } }
+ %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } }
%span.dropdown-toggle-text
Group:
- if @group.present?
@@ -18,7 +18,7 @@
= dropdown_loading
.dropdown.project-filter
- %button.dropdown-menu-toggle.btn.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } }
+ %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } }
%span.dropdown-toggle-text
Project:
- if @project.present?
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 6f0a0ea36ec..9e8adc82583 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,11 +1,13 @@
-- blob = parse_search_result(blob)
+- file_name, blob = blob
.blob-result
.file-holder
.file-title
- - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(blob.ref, blob.filename))
+ - ref = @search_results.repository_ref
+ - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(ref, file_name))
= link_to blob_link do
%i.fa.fa-file
%strong
- = blob.filename
- .file-content.code.term
- = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
+ = file_name
+ - if blob
+ .file-content.code.term
+ = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index c414acb6a11..027d42396b4 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -14,7 +14,7 @@
= link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project)
.snippet-info
- = "##{snippet_title.id}"
+ = snippet_title.to_reference
%span
by
= link_to user_snippets_path(snippet_title.author) do
diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml
index 000532b1c9a..94295970acf 100644
--- a/app/views/shared/_choose_group_avatar_button.html.haml
+++ b/app/views/shared/_choose_group_avatar_button.html.haml
@@ -1,4 +1,4 @@
-%a.choose-btn.btn.btn-sm.js-choose-group-avatar-button
+%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button{ type: 'button' }
%i.fa.fa-paperclip
%span Choose File ...
&nbsp;
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 3b82d8e686f..03684389742 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -2,12 +2,12 @@
.git-clone-holder.input-group
.input-group-btn
- -if allowed_protocols_present?
+ - if allowed_protocols_present?
.clone-dropdown-btn.btn.btn-static
%span
= enabled_project_button(project, enabled_protocol)
- else
- %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', data: { toggle: 'dropdown' }}
+ %a#clone-dropdown.clone-dropdown-btn.btn{ href: '#', data: { toggle: 'dropdown' } }
%span
= default_clone_protocol.upcase
= icon('caret-down')
@@ -19,7 +19,7 @@
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
.input-group-btn
- = clipboard_button(clipboard_target: '#project_clone')
+ = clipboard_button(clipboard_target: '#project_clone', title: "Copy URL to clipboard")
:javascript
$('ul.clone-options-dropdown a').on('click',function(e){
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 0a38327baa2..c196bc06b17 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -1,5 +1,6 @@
.form-group.commit_message-group
- nonce = SecureRandom.hex
+ - descriptions = local_assigns.slice(:message_with_description, :message_without_description)
= label_tag "commit_message-#{nonce}", class: 'control-label' do
Commit message
.col-sm-10
@@ -8,9 +9,17 @@
= text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]),
class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
+ data: descriptions,
required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
- if local_assigns[:hint]
%p.hint
Try to keep the first line under 52 characters
and the others under 72.
+ - if descriptions.present?
+ %p.hint.js-with-description-hint
+ = link_to "#", class: "js-with-description-link" do
+ Include description in commit message
+ %p.hint.js-without-description-hint.hide
+ = link_to "#", class: "js-without-description-link" do
+ Don't include description in commit message
diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml
index b0fc60573f7..e7cb93b17a7 100644
--- a/app/views/shared/_confirm_modal.html.haml
+++ b/app/views/shared/_confirm_modal.html.haml
@@ -1,8 +1,8 @@
-#modal-confirm-danger.modal{tabindex: -1}
+#modal-confirm-danger.modal{ tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title
Confirmation required
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index c367ae336db..e50ab5fea09 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -5,5 +5,7 @@
- if event_filter_visible(:merge_requests)
= event_filter_link EventFilter.merged, 'Merge events'
- if event_filter_visible(:issues)
+ = event_filter_link EventFilter.issue, 'Issue events'
+ - if comments_visible?
= event_filter_link EventFilter.comments, 'Comments'
= event_filter_link EventFilter.team, 'Team'
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index e26693bf5b9..8d64cb5d698 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -9,8 +9,8 @@
- offset = defined?(first_line_number) ? first_line_number : 1
- i = index + offset
-# We're not using `link_to` because it is too slow once we get to thousands of lines.
- %a.diff-line-num{href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i}
+ %a.diff-line-num{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i }
= link_icon
= i
- .blob-content{data: {blob_id: blob.id}}
+ .blob-content{ data: { blob_id: blob.id } }
= highlight(blob.path, blob.data, repository: repository, plain: blob.no_highlighting?)
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index ba25e09d638..0bc851b4256 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -13,8 +13,9 @@
.input-group-addon
= root_url
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
- autofocus: local_assigns[:autofocus] || false, pattern: "[a-zA-Z0-9-_]+",
- required: true, title: 'Please choose a group name with no special characters.'
+ autofocus: local_assigns[:autofocus] || false, required: true,
+ pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE,
+ title: 'Please choose a group name with no special characters.'
- if @group.persisted?
.alert.alert-warning.prepend-top-10
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index a5df502d7b5..26b349e8a62 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,4 +1,4 @@
-- if @issues.reorder(nil).any?
+- if @issues.to_a.any?
- @issues.group_by(&:project).each do |group|
.panel.panel-default.panel-small
- project = group[0]
@@ -13,4 +13,4 @@
= render 'projects/issues/issue', issue: issue
= paginate @issues, theme: "gitlab"
- else
- .nothing-here-block No issues to show
+ = render 'shared/empty_states/issues'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 6ccdef0df46..f11f4471a9d 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,9 +1,10 @@
- label_css_id = dom_id(label)
- open_issues_count = label.open_issues_count(current_user)
- open_merge_requests_count = label.open_merge_requests_count(current_user)
+- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
-%li{id: label_css_id, data: { id: label.id } }
+%li{ id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label
.visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
@@ -18,10 +19,19 @@
%li
= link_to_label(label, subject: subject) do
= pluralize open_issues_count, 'open issue'
- - if current_user
- %li.label-subscription{ data: toggle_subscription_data(label) }
- %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
- %span= label_subscription_toggle_button_text(label)
+ - if current_user && defined?(@project)
+ %li.label-subscription
+ - if label.is_a?(ProjectLabel)
+ %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %span= label_subscription_toggle_button_text(label, @project)
+ - else
+ %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } }
+ %span Unsubscribe
+ %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %span Subscribe at project level
+ %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } }
+ %span Subscribe at group level
+
- if can?(current_user, :admin_label, label)
%li
= link_to 'Edit', edit_label_path(label)
@@ -34,12 +44,27 @@
= link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do
= pluralize open_issues_count, 'open issue'
- - if current_user
- .label-subscription.inline{ data: toggle_subscription_data(label) }
- %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
- %span.sr-only= label_subscription_toggle_button_text(label)
- = icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel))
- = icon('spinner spin', class: 'label-subscribe-button-loading')
+ - if current_user && defined?(@project)
+ .label-subscription.inline
+ - if label.is_a?(ProjectLabel)
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', title: label_subscription_toggle_button_text(label, @project), data: { toggle: 'tooltip', status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %span= label_subscription_toggle_button_text(label, @project)
+ = icon('spinner spin', class: 'label-subscribe-button-loading')
+ - else
+ %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', class: ('hidden' if status.unsubscribed?), title: 'Unsubscribe', data: { toggle: 'tooltip', url: group_label_unsubscribe_path(label, @project) } }
+ %span Unsubscribe
+ = icon('spinner spin', class: 'label-subscribe-button-loading')
+
+ .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %span Subscribe
+ = icon('chevron-down')
+ %ul.dropdown-menu
+ %li
+ %a.js-subscribe-button{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ Project level
+ %a.js-subscribe-button{ data: { url: toggle_subscription_group_label_path(label.group, label) } }
+ Group level
- if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
@@ -49,6 +74,10 @@
%span.sr-only Delete
= icon('trash-o')
- - if current_user && label.is_a?(ProjectLabel)
- :javascript
- new Subscription('##{dom_id(label)} .label-subscription');
+ - if current_user && defined?(@project)
+ - if label.is_a?(ProjectLabel)
+ :javascript
+ new gl.ProjectLabelSubscription('##{dom_id(label)} .label-subscription');
+ - else
+ :javascript
+ new gl.GroupLabelSubscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index ca3178395c1..2f3605b4d27 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,4 +1,4 @@
-- if @merge_requests.reorder(nil).any?
+- if @merge_requests.to_a.any?
- @merge_requests.group_by(&:target_project).each do |group|
.panel.panel-default.panel-small
- project = group[0]
diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml
index b8eef15fbec..5e9007aaaac 100644
--- a/app/views/shared/_milestone_expired.html.haml
+++ b/app/views/shared/_milestone_expired.html.haml
@@ -1,5 +1,7 @@
- if milestone.expired? and not milestone.closed?
%span.cred (Expired)
-- if milestone.expires_at
+- if milestone.upcoming?
+ %span.clgray (Upcoming)
+- if milestone.due_date || milestone.start_date
%span
- = milestone.expires_at
+ = milestone_date_range(milestone)
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 73d288e2236..39294fe1a09 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -2,17 +2,17 @@
- counts = milestone_counts(@project.milestones)
%ul.nav-links
- %li{class: milestone_class_for_state(params[:state], 'opened', true)}
+ %li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do
Open
- if @project
%span.badge #{counts[:opened]}
- %li{class: milestone_class_for_state(params[:state], 'closed')}
+ %li{ class: milestone_class_for_state(params[:state], 'closed') }>
= link_to milestones_filter_path(state: 'closed') do
Closed
- if @project
%span.badge #{counts[:closed]}
- %li{class: milestone_class_for_state(params[:state], 'all')}
+ %li{ class: milestone_class_for_state(params[:state], 'all') }>
= link_to milestones_filter_path(state: 'all') do
All
- if @project
diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml
index 4e3b1b3a571..61646f150c1 100644
--- a/app/views/shared/_nav_scroll.html.haml
+++ b/app/views/shared/_nav_scroll.html.haml
@@ -1,4 +1,4 @@
.fade-left
= icon('angle-left')
.fade-right
- = icon('angle-right') \ No newline at end of file
+ = icon('angle-right')
diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml
index a43bf33751a..ed6fc76c61e 100644
--- a/app/views/shared/_no_password.html.haml
+++ b/app/views/shared/_no_password.html.haml
@@ -1,8 +1,8 @@
- if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password?
- .no-password-message.alert.alert-warning.hidden-xs
+ .no-password-message.alert.alert-warning
You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account
- .pull-right
+ .alert-link-group
= link_to "Don't show again", profile_path(user: {hide_no_password: true}), method: :put
|
= link_to 'Remind later', '#', class: 'hide-no-password-message'
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index bb5fff2d3bb..d663fa13d10 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,8 +1,8 @@
- if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key?
- .no-ssh-key-message.alert.alert-warning.hidden-xs
+ .no-ssh-key-message.alert.alert-warning
You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile
- .pull-right
+ .alert-link-group
= link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link'
|
= link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link'
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 5254d265918..9c5053dace5 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -1,46 +1,50 @@
= form_errors(@service)
-- if @service.help.present?
+- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
+ = render "projects/services/#{@service.to_param}/help", subject: subject
+- elsif @service.help.present?
.well
= preserve do
= markdown @service.help
-.form-group
- = form.label :active, "Active", class: "control-label"
- .col-sm-10
- = form.check_box :active
-
-.form-group
- = form.label :url, "Trigger", class: 'control-label'
-
- .col-sm-10
- - @service.supported_events.each do |event|
- %div
- = form.check_box service_event_field_name(event), class: 'pull-left'
- .prepend-left-20
- = form.label service_event_field_name(event), class: 'list-label' do
- %strong
- = event.humanize
-
- - field = @service.event_field(event)
-
- - if field
- %p
- = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
-
- %p.light
- = service_event_description(event)
-
-- @service.global_fields.each do |field|
- - type = field[:type]
-
- - if type == 'fieldset'
- - fields = field[:fields]
- - legend = field[:legend]
-
- %fieldset
- %legend= legend
- - fields.each do |subfield|
- = render 'shared/field', form: form, field: subfield
- - else
- = render 'shared/field', form: form, field: field
+.service-settings
+ .form-group
+ = form.label :active, "Active", class: "control-label"
+ .col-sm-10
+ = form.check_box :active
+
+ - if @service.supported_events.present?
+ .form-group
+ = form.label :url, "Trigger", class: 'control-label'
+
+ .col-sm-10
+ - @service.supported_events.each do |event|
+ %div
+ = form.check_box service_event_field_name(event), class: 'pull-left'
+ .prepend-left-20
+ = form.label service_event_field_name(event), class: 'list-label' do
+ %strong
+ = event.humanize
+
+ - field = @service.event_field(event)
+
+ - if field
+ %p
+ = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
+
+ %p.light
+ = service_event_description(event)
+
+ - @service.global_fields.each do |field|
+ - type = field[:type]
+
+ - if type == 'fieldset'
+ - fields = field[:fields]
+ - legend = field[:legend]
+
+ %fieldset
+ %legend= legend
+ - fields.each do |subfield|
+ = render 'shared/field', form: form, field: subfield
+ - else
+ = render 'shared/field', form: form, field: field
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 68e05cb72e1..0ce0d759e86 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,11 +1,11 @@
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- = icon('caret-down')
+ = icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
= link_to page_filter_path(sort: sort_value_priority, label: true) do
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 60353aee7f1..b6047ece592 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,23 +1,23 @@
%ul.nav-links
- %li{ class: ('active' if scope.nil?) }
+ %li{ class: ('active' if scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(all_builds.count(:id))
- %li{ class: ('active' if scope == 'pending') }
+ %li{ class: ('active' if scope == 'pending') }>
= link_to build_path_proc.call('pending') do
Pending
%span.badge
= number_with_delimiter(all_builds.pending.count(:id))
- %li{ class: ('active' if scope == 'running') }
+ %li{ class: ('active' if scope == 'running') }>
= link_to build_path_proc.call('running') do
Running
%span.badge
= number_with_delimiter(all_builds.running.count(:id))
- %li{ class: ('active' if scope == 'finished') }
+ %li{ class: ('active' if scope == 'finished') }>
= link_to build_path_proc.call('finished') do
Finished
%span.badge
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
new file mode 100644
index 00000000000..e2033654018
--- /dev/null
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -0,0 +1,22 @@
+- button_path = local_assigns.fetch(:button_path, false)
+- project_select_button = local_assigns.fetch(:project_select_button, false)
+- has_button = button_path || project_select_button
+
+.row.empty-state
+ .pull-right.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
+ .svg-content
+ = render 'shared/empty_states/icons/issues.svg'
+ .col-xs-12{ class: "#{'col-sm-6' if has_button}" }
+ .text-content
+ - if has_button && current_user
+ %h4
+ The Issue Tracker is the place to add things that need to be improved or solved in a project
+ %p
+ Issues can be bugs, tasks or ideas to be discussed.
+ Also, issues are searchable and filterable.
+ - if project_select_button
+ = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
+ - else
+ = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
+ - else
+ %h4.text-center There are no issues to show.
diff --git a/app/views/shared/empty_states/icons/_issues.svg b/app/views/shared/empty_states/icons/_issues.svg
new file mode 100644
index 00000000000..2e92bf19579
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_issues.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="790 253 425 254" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="25" height="8.9423" x="25" y="88.4231" rx="2"/><mask id="h" width="25" height="8.9423" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M16 29.8013h43V91.404H16z"/><mask id="i" width="43" height="61.6026" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M57 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5747 24.863c.1564 1.0866-.253 1.2572-.912.384L66 86.436l-9-6.9552"/><mask id="j" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><path id="d" d="M.2496 60.6026l13.1868 9.3587c.449.3188.876 1.0142.9556 1.5673l3.5748 24.863c.1562 1.0866-.2532 1.2572-.9123.384L9.2495 86.436l-9-6.9552"/><mask id="k" width="17.7504" height="36.7306" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><path id="e" d="M16 29.8013L35.786 1.4556c.9466-1.3562 2.4792-1.3594 3.428 0L59 29.8013"/><mask id="l" width="43" height="29.364" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><rect id="f" width="26.2653" height="35.5088" x="6.3673" rx="13.1327"/><mask id="m" width="26.2653" height="35.5088" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><rect id="g" width="16.8367" height="22.386" x="4.0816" rx="8.4184"/><mask id="n" width="16.8367" height="22.386" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(792.000000, 255.000000)"><g fill="#FDE5D8"><path d="M225.4372 59.5866c-.059.5897-.1323 1.2698-.2203 2.0305-.252 2.1764-.5717 4.559-.9653 7.07-.1283.8185.4312 1.586 1.2496 1.7143.8185.1283 1.586-.4312 1.7142-1.2497.4-2.5528.7253-4.975.9815-7.1898.0898-.7762.1646-1.4715.2252-2.0762.0366-.365.0604-.62.0722-.7557.0717-.8254-.539-1.5526-1.3645-1.6244-.8254-.0717-1.5526.539-1.6244 1.3645-.0106.1228-.0332.365-.0684.7166zM219.8738 87.9413c-.2563.7878.1745 1.6342.9622 1.8906.7878.2562 1.6342-.1745 1.8906-.9623.975-2.9962 1.849-6.2827 2.6287-9.797.1794-.8086-.3308-1.6097-1.1395-1.789-.8088-.1795-1.61.3306-1.7893 1.1394-.76 3.4256-1.6096 6.6206-2.5527 9.5183zM209.9266 103.166c-.781.2766-1.1897 1.134-.913 1.9148.2765.781 1.1338 1.1897 1.9147.913 2.9792-1.0552 5.5414-3.679 7.7796-7.6272.4084-.7207.1554-1.636-.5653-2.0447-.7207-.4086-1.636-.1556-2.0446.565-1.9152 3.3786-3.9945 5.508-6.1714 6.279zM190.439 107.5834c-.7636.3214-1.122 1.201-.8005 1.9645.3215.7634 1.201 1.1217 1.9645.8003 3.1204-1.314 6.2717-2.3243 9.258-2.9816.809-.178 1.3205-.9783 1.1424-1.7874-.178-.809-.9783-1.3205-1.7874-1.1424-3.1666.697-6.4914 1.763-9.777 3.1464zM173.231 118.6257c-.6005.5706-.6248 1.52-.0542 2.1206s1.52.625 2.1206.0543c2.282-2.1682 4.8656-4.162 7.6758-5.946.6994-.444.9064-1.371.4624-2.0704-.444-.6994-1.371-.9064-2.0704-.4624-2.9698 1.8854-5.707 3.998-8.1342 6.304zM162.4543 136.2492c-.2022.8034.2852 1.6185 1.0885 1.8207.8034.202 1.6186-.2853 1.8208-1.0886.7688-3.0547 2.0416-5.9768 3.781-8.7486.4403-.7018.2284-1.6276-.4733-2.068-.7017-.4402-1.6275-.2283-2.068.4734-1.9026 3.0322-3.3016 6.2438-4.149 9.611zM162.1894 156.693c.1036.822.854 1.4042 1.676 1.3006.8218-.1037 1.404-.854 1.3004-1.676-.367-2.9097-.5796-6.1364-.6444-9.8167-.0146-.8284-.698-1.488-1.5262-1.4734-.8283.0146-1.488.698-1.4733 1.5262.0665 3.783.286 7.1162.6674 10.1393zM168.408 176.1653c.3876.7322 1.2953 1.0117 2.0275.6242.7322-.3875 1.0117-1.2952.6242-2.0274-1.6733-3.162-2.9028-5.9954-3.8477-8.943-.2528-.789-1.0973-1.2235-1.8862-.9706-.789.2528-1.2234 1.0974-.9706 1.8863 1.0025 3.1275 2.3014 6.121 4.053 9.4306zM175.9738 188.9357c1.056 1.7165 1.8892 3.0806 2.7307 4.474.4283.709 1.3503.9368 2.0595.5085.709-.4283.9368-1.3503.5085-2.0595-.8464-1.4014-1.6836-2.772-2.7434-4.4948.0808.131-1.9545-3.1733-2.486-4.0405-.4328-.7063-1.3563-.928-2.0627-.495-.7063.4327-.928 1.3563-.495 2.0626.5334.8707 2.5708 4.1785 2.4885 4.0447zM184.83 211.3822c.011.8284.6912 1.491 1.5196 1.4803.8283-.0108 1.491-.691 1.4803-1.5194-.046-3.519-.6604-6.996-1.8367-10.3262-.276-.7812-1.1328-1.1908-1.914-.915-.781.276-1.1906 1.133-.9147 1.914 1.0668 3.0206 1.624 6.1733 1.6655 9.3664zM179.3467 229.4095c-.459.6896-.2723 1.6208.4173 2.08.6896.459 1.6208.272 2.08-.4175 1.966-2.9533 3.4756-6.124 4.4877-9.4165.2434-.7918-.2012-1.631-.993-1.8745-.792-.2434-1.6312.2012-1.8746.993-.9264 3.014-2.3108 5.922-4.1173 8.6355z"/></g><g transform="translate(336.866969, 147.225953) rotate(-300.000000) translate(-336.866969, -147.225953) translate(299.366969, 69.725953)"><path stroke="#FDE5D8" stroke-width="3" d="M19 154l10-52.6603m16 0L55 154" stroke-linecap="round"/><rect width="3" height="38.75" x="35" y="99.3526" fill="#FDE5D8" rx="1.5"/><use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#h)" xlink:href="#a"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#i)" xlink:href="#b"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#j)" xlink:href="#c"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#k)" transform="translate(9.124810, 78.967887) scale(-1, 1) translate(-9.124810, -78.967887)" xlink:href="#d"/><use stroke="#FDE5D8" stroke-width="6" mask="url(#l)" xlink:href="#e"/><ellipse cx="28.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="34.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="40.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="46.5" cy="82.9583" fill="#FC8A51" rx="1.5" ry="1.4904"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="10.5" ry="10.4327"/><ellipse cx="37.5" cy="55.1378" stroke="#FDE5D8" stroke-width="3" rx="5.5" ry="5.4647"/></g><path fill="#EEE" d="M96.0426 37.2106c-.1512 1.6874.0814 3.815.997 6.146.2046.5207.7936.7774 1.3155.5733.522-.2043.7793-.792.5747-1.313-.7912-2.0142-.99-3.832-.865-5.226.0102-.1143.0195-.186.0238-.2113.092-.552-.2814-1.0738-.8344-1.1658-.553-.092-1.076.2808-1.168.8326-.0126.075-.0285.1975-.0434.364zM107.5302 52.8934c.4913.239 1.098.0626 1.355-.394.2572-.4566.0674-1.0205-.4238-1.2595-1.8668-.9083-3.4584-1.9152-4.7943-3.0075-.4162-.3404-1.0506-.3026-1.4168.0843-.3663.387-.3256.9766.0907 1.317 1.4583 1.1925 3.1828 2.2835 5.1893 3.2596zM120.661 58.9533c.5467.171 1.1257-.1425 1.2933-.7003.1675-.5577-.1397-1.1484-.6864-1.3194-3.0283-.9472-4.1984-1.3178-5.915-1.8824-.544-.179-1.1274.126-1.3028.6813-.1754.5552.1235 1.1504.6677 1.3294 1.729.5686 2.9053.941 5.943 1.8913zM132.5954 62.881c.449.246 1.022.0983 1.2798-.33.258-.4282.103-.975-.3458-1.221-1.4942-.819-3.1928-1.545-5.2675-2.2746-.486-.1708-1.025.0664-1.204.53-.179.4634.0697.9776.5555 1.1484 1.9832.6973 3.5892 1.3838 4.982 2.1472zM141.9774 73.383c.205.4938.809.742 1.3485.5543.5395-.1878.8106-.7404.6055-1.2344-.8504-2.0482-1.853-3.7962-3.0375-5.3046-.337-.429-.99-.527-1.4588-.2184-.4687.3085-.5755.9064-.2386 1.3354 1.0743 1.368 1.9926 2.9692 2.7808 4.8675zM144.609 87.025c.0183.5535.5682.99 1.2283.9746.66-.0153 1.1805-.4764 1.1622-1.03-.0725-2.2033-.2693-4.206-.622-6.1198-.1008-.5473-.7115-.9225-1.3642-.838-.6526.0846-1.1.597-.999 1.1442.336 1.8248.5248 3.745.5947 5.869z"/><path fill="#E5E5E5" d="M144.1423 95.7297c-.0863 2.5442-.1214 3.769-.1422 5.2548-.0076.5523.3963 1.007.9022 1.0154.506.0083.9223-.4326.93-.985.0205-1.4668.0554-2.6812.1412-5.2113l.026-.7667c.0185-.552-.3764-1.016-.882-1.0363-.5056-.0203-.9306.411-.949.963l-.026.766zM144.939 115.201c.1196.5447.6727.8925 1.2355.7768.5628-.1157.922-.651.8026-1.1957-.417-1.9-.7104-3.84-.8976-5.8637-.0513-.5545-.5574-.964-1.1305-.9142-.573.0497-.996.5396-.9448 1.0942.1944 2.1015.4998 4.121.9348 6.103zM149.995 127.5248c.296.454.9528.61 1.4668.3485.514-.2614.6907-.8413.3947-1.2952-1.0787-1.6535-2.0046-3.3145-2.7896-4.9916-.2266-.484-.8547-.7143-1.403-.5142-.548.2-.809.7546-.5823 1.2387.8208 1.7534 1.788 3.4886 2.9134 5.2138zM154.8088 135.226c1.0587 1.232 2.242 2.4097 3.543 3.531.404.3482 1.0276.3186 1.393-.066.3657-.3843.3346-.978-.0692-1.3262-1.2296-1.0597-2.345-2.17-3.3402-3.328-.195-.227-.3872-.4542-.5764-.6813-.3385-.4063-.9588-.4744-1.3856-.1522-.4267.3223-.4983.913-.1598 1.3192.1954.2346.3938.469.5952.7034zM170.634 146.9026c.4806.242 1.0517.0176 1.2758-.501.224-.5188.0162-1.1354-.4642-1.3773-1.7563-.8842-3.422-1.8432-4.9857-2.8726-.4527-.298-1.0434-.1435-1.3195.3452-.276.4885-.133 1.126.3198 1.424 1.6256 1.0704 3.354 2.0655 5.1738 2.9816z"/><path fill="#EEE" d="M184.7334 151.9698c.5527.1412 1.1072-.2262 1.2385-.8206.1312-.5944-.2104-1.1908-.763-1.332-2.001-.5114-3.9602-1.1002-5.8632-1.763-.5405-.1883-1.1205.1303-1.2955.7115-.175.5813.1212 1.205.6616 1.3934 1.9557.6813 3.9676 1.286 6.0214 1.8108zM197.9337 153.9977c.5532.04 1.0297-.445 1.0643-1.083.0346-.6383-.3857-1.188-.939-1.228-1.973-.1424-3.952-.3682-5.9206-.676-.5492-.086-1.0547.358-1.1292.9917-.0744.6336.3105 1.2168.8597 1.3027 2.0164.3154 4.0433.5467 6.0647.6927zM212.1213 152.6062c.5493-.055.9392-.4576.871-.8994-.0684-.442-.569-.7555-1.1184-.7006-1.9168.1917-3.893.3194-5.9104.382-.553.0173-.9842.392-.9628.8368.0213.445.487.7916 1.0402.7744 2.0737-.0645 4.1064-.1957 6.0803-.3932zM226.3665 149.949c.5293-.22.7755-.8162.5497-1.332-.2257-.5155-.838-.7553-1.3672-.5354-1.7815.74-3.7143 1.3827-5.7772 1.923-.5558.1454-.8852.7023-.7358 1.2436.1494.5414.721.8623 1.2768.7168 2.1547-.5643 4.1797-1.2376 6.0537-2.016zM237.8486 140.4168c.292-.4344.1488-1.006-.3202-1.2766-.469-.2706-1.086-.1378-1.3782.2967-.9575 1.4237-2.225 2.7337-3.7847 3.9202-.427.3248-.4888.9087-.138 1.3042.3505.3955.981.4528 1.408.128 1.723-1.3107 3.1363-2.7714 4.213-4.3726zM245.6725 130.6874c.3987-.3503.439-.9587.09-1.3588-.3492-.4-.9554-.4405-1.3542-.0902-1.5048 1.3222-2.8978 2.7094-4.1698 4.1635-.3497.3995-.3102 1.008.088 1.3587.3983.3508 1.0046.3113 1.3542-.0884 1.2153-1.389 2.5487-2.717 3.9918-3.985zM257.4814 122.8697c.476-.2568.657-.8577.4047-1.342-.2523-.4843-.8428-.6687-1.3188-.4118-1.7682.9542-3.4795 1.973-5.1228 3.0587-.4518.2985-.5803.9133-.287 1.373.2934.46.8975.5906 1.3494.292 1.5938-1.0528 3.2557-2.0423 4.9746-2.97zM270.276 116.9216c.5503-.1682.8513-.724.6723-1.241-.179-.5173-.77-.8003-1.3204-.632-1.9296.5898-3.932 1.2728-5.975 2.054-.536.205-.7936.7797-.5754 1.2835.218.504.8294.746 1.3654.541 1.9947-.7628 3.95-1.4298 5.833-2.0054z"/><circle cx="145" cy="90" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><circle cx="238" cy="138" r="5" fill="#FFF" stroke="#EEE" stroke-width="2"/><path stroke="#B5A7DD" stroke-width="3" d="M20.0605 56s-17.4698 33-12 53c5.4697 20 17 32 38 44S78.5 148 107 159s29 43 29 43" stroke-linecap="round" stroke-dasharray="8 10"/><g stroke="#EEE" stroke-width="3" transform="translate(108.000000, 173.000000)"><path fill="#FFF" d="M154 77c0-42.526-34.474-77-77-77S0 34.474 0 77" stroke-linecap="round"/><circle cx="108" cy="41" r="16"/><circle cx="42.5" cy="30.5" r="8.5"/><circle cx="22" cy="58" r="5"/></g><g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(19.897959, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g fill="#FC8A51" transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(12.602041, 6.000000) scale(-1, 1) translate(-12.602041, -6.000000) translate(6.102041, -0.000000)"><path d="M.398 11.2982h2.3877c0-4.234 3.3853-7.6666 7.5612-7.6666v-2.421C4.8522 1.2105.398 5.727.398 11.298z"/><ellipse cx="10.7449" cy="2.0175" rx="1.9898" ry="2.0175"/></g><g transform="translate(235.917801, 27.746228) rotate(-345.000000) translate(-235.917801, -27.746228) translate(216.417801, 4.246228) translate(0.000000, 10.491228)"><g fill="#FC8A51" transform="translate(29.448980, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><g fill="#FC8A51" transform="translate(5.051020, 21.298246) scale(-1, 1) translate(-5.051020, -21.298246) translate(0.551020, 11.298246)"><rect width="7.9592" height="2" x=".7959" y="8.8772" rx="1"/><rect width="7.9592" height="2" x=".7959" y="16.1404" transform="translate(4.775510, 17.140351) rotate(-345.000000) translate(-4.775510, -17.140351)" rx="1"/><rect width="7.9592" height="2" x=".9151" y="1.8072" transform="translate(4.894667, 2.807217) rotate(-15.000000) translate(-4.894667, -2.807217)" rx="1"/></g><use stroke="#FC8A51" stroke-width="6" mask="url(#m)" xlink:href="#f"/><path fill="#FC8A51" d="M7.1633 12.9123H31.041v3H7.1632z"/></g></g><g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(12.755102, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g fill="#EEE" transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(7.744898, 4.000000) scale(-1, 1) translate(-7.744898, -4.000000) translate(3.244898, 0.000000)"><path d="M.255 7.1228h1.5307c0-2.6694 2.17-4.8333 4.847-4.8333V.7632C3.1104.7632.255 3.6105.255 7.1228z"/><ellipse cx="6.8878" cy="1.2719" rx="1.2755" ry="1.2719"/></g><g transform="translate(92.956359, 18.724125) scale(-1, 1) rotate(-345.000000) translate(-92.956359, -18.724125) translate(80.456359, 3.724125) translate(0.000000, 6.614035)"><g fill="#EEE" transform="translate(18.877551, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><g fill="#EEE" transform="translate(3.122449, 13.622807) scale(-1, 1) translate(-3.122449, -13.622807) translate(0.122449, 7.122807)"><rect width="5.102" height="2" x=".5102" y="5.5965" rx="1"/><rect width="5.102" height="2" x=".5102" y="10.1754" transform="translate(3.061224, 11.175439) rotate(-345.000000) translate(-3.061224, -11.175439)" rx="1"/><rect width="5.102" height="2" x=".5866" y="1.1393" transform="translate(3.137607, 2.139333) rotate(-15.000000) translate(-3.137607, -2.139333)" rx="1"/></g><use stroke="#EEE" stroke-width="4" mask="url(#n)" xlink:href="#g"/><path fill="#EEE" d="M4.5918 8.1404h15.306v2H4.592z"/></g></g><g fill="#FFF" transform="translate(0.000000, 103.000000)"><circle cx="8.5" cy="8.5" r="8.5" stroke="#B5A7DD" stroke-width="4"/><circle cx="171.5" cy="20.5" r="6.5"/></g><g><g transform="translate(39.000000, 142.000000)"><ellipse cx="12.5" cy="12.5" fill="#FFF" stroke="#6B4FBB" stroke-width="4" rx="12.5" ry="12.5"/><path fill="#FC8A51" d="M10.7322 13.475l-1.7665-1.7667c-.5873-.5873-1.5368-.587-2.1226-.0012-.5897.59-.585 1.5362.0013 2.1226l2.826 2.826.0007.0007.0006.0006c.5898.5897 1.534.587 2.118.003l6.3704-6.3703c.577-.577.5826-1.5323-.003-2.118-.59-.59-1.5343-.5873-2.1183-.0033l-5.3065 5.3065z"/></g></g><circle cx="171.5" cy="122.5" r="6.5" fill="#FFF" stroke="#FC8A51" stroke-width="3"/><circle cx="22" cy="52" r="6" fill="#FFF" stroke="#B5A7DD" stroke-width="3"/><path fill="#FFF" stroke="#B5A7DD" stroke-width="3.6" d="M188.151 141.596c8.7045-7.7456 11.0126-20.9255 4.8625-31.5777-7.0208-12.1604-22.4055-16.422-34.363-9.5183-11.9572 6.9036-15.959 22.358-8.9382 34.5183 6.2353 10.8 19.068 15.3695 30.2375 11.4206l10.8992 18.8778c1.3167 2.2807 4.2302 3.063 6.5078 1.748 2.273-1.3122 3.0567-4.2295 1.74-6.51l-10.9458-18.9587zm-8.4343-4.6086c7.8576-4.5366 10.4874-14.6923 5.8738-22.6834-4.6137-7.991-14.7237-10.7915-22.5814-6.255-7.8575 4.5368-10.4873 14.6925-5.8737 22.6836 4.6137 7.991 14.7237 10.7915 22.5814 6.2548z"/></g></svg>
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 19221e3391f..f9a7aa4e29b 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -21,14 +21,14 @@
= icon('users')
= number_with_delimiter(group.users.count)
- %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) }
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
- = group.name
+ = group.full_name
- if group_member
as
diff --git a/app/views/shared/icons/_delta.svg b/app/views/shared/icons/_delta.svg
new file mode 100644
index 00000000000..7c0c0d3999c
--- /dev/null
+++ b/app/views/shared/icons/_delta.svg
@@ -0,0 +1,3 @@
+<svg width="14px" height="10px" viewBox="322 21 14 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <path d="M330.078605,22.8166945 L335.259532,29.6235062 C335.615145,30.0907182 335.412062,30.4694683 334.822641,30.4694683 L331.657805,30.4694683 L324.04678,30.4694683 C323.449879,30.4694683 323.260751,30.0822112 323.609889,29.6235062 L328.790816,22.8166945 C329.146429,22.3494825 329.729467,22.3579895 330.078605,22.8166945 Z" id="delta" stroke="#5C5C5C" stroke-width="1" fill="none"></path>
+</svg>
diff --git a/app/views/shared/icons/_go_logo.svg.erb b/app/views/shared/icons/_go_logo.svg.erb
new file mode 100644
index 00000000000..5052651c110
--- /dev/null
+++ b/app/views/shared/icons/_go_logo.svg.erb
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16"><g fill-rule="evenodd" transform="translate(0 1)"><path d="m14 15.01h1v-8.02c0-3.862-3.134-6.991-7-6.991-3.858 0-7 3.13-7 6.991v8.02h1v-8.02c0-3.306 2.691-5.991 6-5.991 3.314 0 6 2.682 6 5.991v8.02m-10.52-13.354c-.366-.402-.894-.655-1.48-.655-1.105 0-2 .895-2 2 0 .868.552 1.606 1.325 1.883.102-.321.226-.631.371-.93-.403-.129-.695-.507-.695-.953 0-.552.448-1 1-1 .306 0 .58.138.764.354.222-.25.461-.483.717-.699m9.04-.002c.366-.401.893-.653 1.479-.653 1.105 0 2 .895 2 2 0 .867-.552 1.606-1.324 1.883-.101-.321-.225-.632-.37-.931.403-.129.694-.507.694-.952 0-.552-.448-1-1-1-.305 0-.579.137-.762.353-.222-.25-.461-.483-.717-.699"/><path d="m5.726 7.04h1.557v.124c0 .283-.033.534-.1.752-.065.202-.175.391-.33.566-.35.394-.795.591-1.335.591-.527 0-.979-.19-1.355-.571-.376-.382-.564-.841-.564-1.377 0-.547.191-1.01.574-1.391.382-.382.848-.574 1.396-.574.295 0 .57.06.825.181.244.12.484.316.72.586l-.405.388c-.309-.412-.686-.618-1.13-.618-.399 0-.733.138-1 .413-.27.27-.405.609-.405 1.015 0 .42.151.766.452 1.037.282.252.587.378.915.378.28 0 .531-.094.754-.283.223-.19.347-.418.373-.683h-.94v-.535m2.884.061c0-.53.194-.986.583-1.367.387-.381.853-.571 1.396-.571.537 0 .998.192 1.382.576.386.384.578.845.578 1.384 0 .542-.194 1-.581 1.379-.389.379-.858.569-1.408.569-.487 0-.923-.168-1.311-.505-.426-.373-.64-.861-.64-1.465m.574.007c0 .417.14.759.42 1.028.278.269.6.403.964.403.395 0 .729-.137 1-.41.272-.277.408-.613.408-1.01 0-.402-.134-.739-.403-1.01-.267-.273-.597-.41-.991-.41-.392 0-.723.137-.993.41-.27.27-.405.604-.405 1m-.184 3.918c.525.026.812.063.812.063.271.025.324-.096.116-.273 0 0-.775-.813-1.933-.813-1.159 0-1.923.813-1.923.813-.211.174-.153.3.12.273 0 0 .286-.037.81-.063v.477c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.252.25c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.478m-1-1.023c.552 0 1-.224 1-.5 0-.276-.448-.5-1-.5-.552 0-1 .224-1 .5 0 .276.448.5 1 .5"/></g></svg>
diff --git a/app/views/shared/icons/_icon_cycle_analytics_overview.svg b/app/views/shared/icons/_icon_cycle_analytics_overview.svg
new file mode 100644
index 00000000000..eea9c975c35
--- /dev/null
+++ b/app/views/shared/icons/_icon_cycle_analytics_overview.svg
@@ -0,0 +1,81 @@
+<svg width="366px" height="229px" viewBox="784 258 366 229" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
+ <desc>Created with Sketch.</desc>
+ <defs>
+ <rect id="path-1" x="35" y="39" width="24" height="21" rx="10"></rect>
+ <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="24" height="21" fill="white">
+ <use xlink:href="#path-1"></use>
+ </mask>
+ <rect id="path-3" x="64.8662386" y="58.3882666" width="10" height="71" rx="5"></rect>
+ <mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="71" fill="white">
+ <use xlink:href="#path-3"></use>
+ </mask>
+ <rect id="path-5" x="18.1550472" y="58.3882666" width="10" height="71" rx="5"></rect>
+ <mask id="mask-6" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="71" fill="white">
+ <use xlink:href="#path-5"></use>
+ </mask>
+ <rect id="path-7" x="24" y="56" width="46" height="10" rx="5"></rect>
+ <mask id="mask-8" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="10" fill="white">
+ <use xlink:href="#path-7"></use>
+ </mask>
+ <rect id="path-9" x="42" y="60" width="10" height="68" rx="5"></rect>
+ <mask id="mask-10" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="68" fill="white">
+ <use xlink:href="#path-9"></use>
+ </mask>
+ <rect id="path-11" x="69" y="12" width="12" height="12" rx="3"></rect>
+ <mask id="mask-12" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="12" height="12" fill="white">
+ <use xlink:href="#path-11"></use>
+ </mask>
+ <rect id="path-13" x="40" y="18" width="14" height="22" rx="6"></rect>
+ <mask id="mask-14" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="14" height="22" fill="white">
+ <use xlink:href="#path-13"></use>
+ </mask>
+ <rect id="path-15" x="41" y="8" width="34" height="20" rx="3"></rect>
+ <mask id="mask-16" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="34" height="20" fill="white">
+ <use xlink:href="#path-15"></use>
+ </mask>
+ <path d="M8,8.00793008 C8,6.34669617 9.34984627,5.0321392 11.0036812,5.07151622 L46.9963188,5.92848378 C48.6552061,5.9679811 50,7.34177063 50,8.99109042 L50,27.0089096 C50,28.6608432 48.6501537,30.0321392 46.9963188,30.0715162 L11.0036812,30.9284838 C9.34479389,30.9679811 8,29.6568766 8,27.9920699 L8,8.00793008 Z" id="path-17"></path>
+ <mask id="mask-18" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="42" height="25.858699" fill="white">
+ <use xlink:href="#path-17"></use>
+ </mask>
+ <rect id="path-19" x="-7.10542736e-15" y="1.77635684e-14" width="16" height="36" rx="3"></rect>
+ <mask id="mask-20" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="16" height="36" fill="white">
+ <use xlink:href="#path-19"></use>
+ </mask>
+ </defs>
+ <g id="Group-7" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(786.000000, 259.000000)">
+ <g id="Group-5" transform="translate(132.727922, 71.000000)">
+ <use id="Rectangle-21" stroke="#EEEEEE" mask="url(#mask-2)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-1"></use>
+ <use id="Rectangle-16-Copy" stroke="#EEEEEE" mask="url(#mask-4)" stroke-width="8" fill="#FFFFFF" transform="translate(69.866239, 93.888267) rotate(-20.000000) translate(-69.866239, -93.888267) " xlink:href="#path-3"></use>
+ <use id="Rectangle-16-Copy-2" stroke="#EEEEEE" mask="url(#mask-6)" stroke-width="8" fill="#FFFFFF" transform="translate(23.155047, 93.888267) scale(-1, 1) rotate(-20.000000) translate(-23.155047, -93.888267) " xlink:href="#path-5"></use>
+ <use id="Rectangle-15" stroke="#EEEEEE" mask="url(#mask-8)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-7"></use>
+ <use id="Rectangle-16" stroke="#EEEEEE" mask="url(#mask-10)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-9"></use>
+ <g id="Group" transform="translate(45.500000, 33.000000) rotate(20.000000) translate(-45.500000, -33.000000) translate(5.000000, 13.000000)">
+ <use id="Rectangle-4" stroke="#EEEEEE" mask="url(#mask-12)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-11"></use>
+ <use id="Rectangle-20" stroke="#EEEEEE" mask="url(#mask-14)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-13"></use>
+ <use id="Rectangle-2" stroke="#EEEEEE" mask="url(#mask-16)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-15"></use>
+ <use id="Rectangle" stroke="#EEEEEE" mask="url(#mask-18)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-17"></use>
+ <rect id="Rectangle-17" fill="#EEEEEE" x="21" y="7" width="3" height="22"></rect>
+ <rect id="Rectangle-17-Copy" fill="#EEEEEE" x="64" y="8" width="3" height="17"></rect>
+ <circle id="Oval-9" fill="#B5A7DD" cx="40" cy="18" r="2"></circle>
+ <circle id="Oval-9-Copy-4" fill="#EEEEEE" cx="47" cy="33" r="2"></circle>
+ <use id="Rectangle-19" stroke="#EEEEEE" mask="url(#mask-20)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-19"></use>
+ </g>
+ </g>
+ <path d="M265.128496,225.286991 C247.289192,194.617726 214.068171,174 176.031622,174 C137.847583,174 104.51649,194.77793 86.7279221,225.644211" id="Oval-10" stroke="#EEEEEE" stroke-width="4" stroke-linecap="round" fill="#FFFFFF"></path>
+ <circle id="Oval-11" stroke="#FDE5D8" stroke-width="4" fill="#FFFFFF" cx="24.5" cy="25.5" r="24.5"></circle>
+ <path d="M24,1.00292933 C24,0.449026756 24.4438648,0 25,0 C25.5522847,0 26,0.437881351 26,1.00292933 L26,5.99707067 C26,6.55097324 25.5561352,7 25,7 C24.4477153,7 24,6.56211865 24,5.99707067 L24,1.00292933 Z M48.46461,17.3244238 C48.9914026,17.1532585 49.5556142,17.4366422 49.7274694,17.9655581 C49.8981348,18.4908122 49.6200365,19.0519274 49.0826439,19.2265369 L44.3329333,20.7698114 C43.8061406,20.9409767 43.241929,20.6575931 43.0700738,20.1286771 C42.8994084,19.6034231 43.1775067,19.0423078 43.7148993,18.8676984 L48.46461,17.3244238 Z M40.5019265,45.6352697 C40.8275022,46.0833863 40.7323394,46.7075538 40.2824166,47.0344419 C39.8356088,47.3590667 39.2160194,47.2679737 38.8838925,46.8108402 L35.9484099,42.770495 C35.6228341,42.3223784 35.717997,41.6982109 36.1679198,41.3713229 C36.6147275,41.0466981 37.234317,41.1377911 37.5664439,41.5949245 L40.5019265,45.6352697 Z M11.1161075,46.8108402 C10.7905317,47.2589568 10.1675063,47.3613299 9.71758344,47.0344419 C9.27077569,46.709817 9.16594665,46.0924031 9.49807352,45.6352697 L12.4335561,41.5949245 C12.7591319,41.1468079 13.3821574,41.0444348 13.8320802,41.3713229 C14.278888,41.6959477 14.383717,42.3133616 14.0515901,42.770495 L11.1161075,46.8108402 Z M0.917356057,19.2265369 C0.390563404,19.0553716 0.100675355,18.4944741 0.272530576,17.9655581 C0.44319595,17.4403041 0.997997482,17.1498144 1.53539005,17.3244238 L6.28510071,18.8676984 C6.81189336,19.0388637 7.10178141,19.5997611 6.92992619,20.1286771 C6.75926082,20.6539311 6.20445928,20.9444208 5.66706672,20.7698114 L0.917356057,19.2265369 Z" id="Rectangle-23" fill="#FDE5D8"></path>
+ <rect id="Rectangle-18" fill="#FC6D26" x="24" y="14" width="3" height="12" rx="1.5"></rect>
+ <rect id="Rectangle-22" fill="#FC6D26" x="24" y="24" width="12" height="3" rx="1.5"></rect>
+ <circle id="Oval-11" fill="#6B4FBB" cx="25.5" cy="25.5" r="2.5"></circle>
+ <path d="M358.949747,6.87474747 L357.453009,7.20729654 C356.9128,7.32732164 356.570654,6.9935311 356.692198,6.44648557 L357.024747,4.94974747 L356.692198,3.45300937 C356.572173,2.91279997 356.905964,2.57065443 357.453009,2.69219839 L358.949747,3.02474747 L360.446486,2.69219839 C360.986695,2.5721733 361.328841,2.90596384 361.207297,3.45300937 L360.874747,4.94974747 L361.207297,6.44648557 C361.327322,6.98669496 360.993531,7.32884051 360.446486,7.20729654 L358.949747,6.87474747 Z" id="Star-Copy-5" fill="#6B4FBB" transform="translate(358.949747, 4.949747) rotate(-315.000000) translate(-358.949747, -4.949747) "></path>
+ <path d="M113.949747,32.8747475 L112.453009,33.2072965 C111.9128,33.3273216 111.570654,32.9935311 111.692198,32.4464856 L112.024747,30.9497475 L111.692198,29.4530094 C111.572173,28.9128 111.905964,28.5706544 112.453009,28.6921984 L113.949747,29.0247475 L115.446486,28.6921984 C115.986695,28.5721733 116.328841,28.9059638 116.207297,29.4530094 L115.874747,30.9497475 L116.207297,32.4464856 C116.327322,32.986695 115.993531,33.3288405 115.446486,33.2072965 L113.949747,32.8747475 Z" id="Star-Copy-7" fill="#B5A7DD" transform="translate(113.949747, 30.949747) rotate(-315.000000) translate(-113.949747, -30.949747) "></path>
+ <path d="M329.949747,211.874747 L328.453009,212.207297 C327.9128,212.327322 327.570654,211.993531 327.692198,211.446486 L328.024747,209.949747 L327.692198,208.453009 C327.572173,207.9128 327.905964,207.570654 328.453009,207.692198 L329.949747,208.024747 L331.446486,207.692198 C331.986695,207.572173 332.328841,207.905964 332.207297,208.453009 L331.874747,209.949747 L332.207297,211.446486 C332.327322,211.986695 331.993531,212.328841 331.446486,212.207297 L329.949747,211.874747 Z" id="Star-Copy-6" fill="#B5A7DD" opacity="0.5" transform="translate(329.949747, 209.949747) rotate(-315.000000) translate(-329.949747, -209.949747) "></path>
+ <path d="M265.363961,54.838961 L263.153969,55.3299826 C262.617155,55.4492534 262.280283,55.1035008 262.397939,54.5739526 L262.888961,52.363961 L262.397939,50.1539694 C262.278669,49.6171548 262.624421,49.2802831 263.153969,49.3979395 L265.363961,49.888961 L267.573953,49.3979395 C268.110767,49.2786686 268.447639,49.6244213 268.329983,50.1539694 L267.838961,52.363961 L268.329983,54.5739526 C268.449253,55.1107673 268.103501,55.4476389 267.573953,55.3299826 L265.363961,54.838961 Z" id="Star-Copy-9" fill="#FC6D26" transform="translate(265.363961, 52.363961) rotate(-315.000000) translate(-265.363961, -52.363961) "></path>
+ <path d="M56.363961,142.838961 L54.1539694,143.329983 C53.6171548,143.449253 53.2802831,143.103501 53.3979395,142.573953 L53.888961,140.363961 L53.3979395,138.153969 C53.2786686,137.617155 53.6244213,137.280283 54.1539694,137.397939 L56.363961,137.888961 L58.5739526,137.397939 C59.1107673,137.278669 59.4476389,137.624421 59.3299826,138.153969 L58.838961,140.363961 L59.3299826,142.573953 C59.4492534,143.110767 59.1035008,143.447639 58.5739526,143.329983 L56.363961,142.838961 Z" id="Star-Copy-8" fill="#6B4FBB" transform="translate(56.363961, 140.363961) rotate(-315.000000) translate(-56.363961, -140.363961) "></path>
+ <g id="Group-6" transform="translate(311.872633, 125.094458) rotate(-345.000000) translate(-311.872633, -125.094458) translate(290.872633, 115.094458)">
+ <circle id="Oval-12" stroke="#FDE5D8" stroke-width="4" fill="#FFFFFF" cx="21" cy="10" r="10"></circle>
+ <ellipse id="Oval-13" fill="#FDE5D8" cx="21" cy="10" rx="21" ry="2"></ellipse>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/shared/icons/_icon_lock.svg b/app/views/shared/icons/_icon_lock.svg
new file mode 100644
index 00000000000..6ec671a76ed
--- /dev/null
+++ b/app/views/shared/icons/_icon_lock.svg
@@ -0,0 +1,25 @@
+<svg width="46px" height="54px" viewBox="227 0 46 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
+ <desc>Created with Sketch.</desc>
+ <defs>
+ <rect id="path-1" x="0" y="20" width="46" height="34" rx="8"></rect>
+ <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="34" fill="white">
+ <use xlink:href="#path-1"></use>
+ </mask>
+ <path d="M29,16 C29,8.2680135 22.7319865,2 15,2 C7.2680135,2 1,8.2680135 1,16" id="path-3"></path>
+ <mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="28" height="14" fill="white">
+ <use xlink:href="#path-3"></use>
+ </mask>
+ </defs>
+ <g id="locker" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(227.000000, 0.000000)">
+ <g id="Group-8">
+ <use id="Rectangle-14" stroke="#B5A7DD" mask="url(#mask-2)" stroke-width="6" xlink:href="#path-1"></use>
+ <g id="Group-7" transform="translate(8.000000, 0.000000)">
+ <use id="Oval-3" stroke="#B5A7DD" mask="url(#mask-4)" stroke-width="6" xlink:href="#path-3"></use>
+ <rect id="Rectangle-13" fill="#B5A7DD" x="1" y="16" width="3" height="6"></rect>
+ <rect id="Rectangle-13-Copy" fill="#B5A7DD" x="26" y="16" width="3" height="6"></rect>
+ </g>
+ <path d="M25,37.4648712 C26.1956027,36.7732524 27,35.4805647 27,34 C27,31.790861 25.209139,30 23,30 C20.790861,30 19,31.790861 19,34 C19,35.4805647 19.8043973,36.7732524 21,37.4648712 L21,41.0026083 C21,42.1041422 21.8954305,43 23,43 C24.1122704,43 25,42.1057373 25,41.0026083 L25,37.4648712 Z" id="Combined-Shape" fill="#6B4FBB"></path>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/shared/icons/_icon_no_data.svg b/app/views/shared/icons/_icon_no_data.svg
new file mode 100644
index 00000000000..ced8653b88c
--- /dev/null
+++ b/app/views/shared/icons/_icon_no_data.svg
@@ -0,0 +1,27 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="211 0 78 36" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <circle id="a" cx="5" cy="31" r="5"/>
+ <mask id="e" width="10" height="10" x="0" y="0" fill="#fff">
+ <use xlink:href="#a"/>
+ </mask>
+ <circle id="b" cx="29" cy="14" r="5"/>
+ <mask id="f" width="10" height="10" x="0" y="0" fill="#fff">
+ <use xlink:href="#b"/>
+ </mask>
+ <circle id="c" cx="53" cy="24" r="5"/>
+ <mask id="g" width="10" height="10" x="0" y="0" fill="#fff">
+ <use xlink:href="#c"/>
+ </mask>
+ <circle id="d" cx="73" cy="5" r="5"/>
+ <mask id="h" width="10" height="10" x="0" y="0" fill="#fff">
+ <use xlink:href="#d"/>
+ </mask>
+ </defs>
+ <g fill="none" fill-rule="evenodd" transform="translate(211)">
+ <path stroke="#B5A7DD" stroke-width="2" d="M5 31l24-17 26 10L73 5" stroke-linecap="round" stroke-dasharray="3 6"/>
+ <use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#e)" xlink:href="#a"/>
+ <use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#f)" xlink:href="#b"/>
+ <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#g)" xlink:href="#c"/>
+ <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#h)" xlink:href="#d"/>
+ </g>
+</svg>
diff --git a/app/views/shared/icons/_icon_status_canceled.svg b/app/views/shared/icons/_icon_status_canceled.svg
index 1b2d0891244..bd5d04e1cd7 100644..100755
--- a/app/views/shared/icons/_icon_status_canceled.svg
+++ b/app/views/shared/icons/_icon_status_canceled.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" class="ci-status-icon-canceled" viewBox="0 0 14 14">
- <g fill="#5C5C5C" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <rect width="8" height="2" x="3" y="6" transform="rotate(45 7 7)" rx=".5"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_canceled_borderless.svg b/app/views/shared/icons/_icon_status_canceled_borderless.svg
new file mode 100644
index 00000000000..bf7fb29185f
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_canceled_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M8.17142857,5.97142857 L15.8714286,13.6714286 C16.1857143,13.9857143 16.1857143,14.4571429 15.8714286,14.7714286 L14.7714286,15.8714286 C14.4571429,16.1857143 13.9857143,16.1857143 13.6714286,15.8714286 L5.97142857,8.17142857 C5.65714286,7.85714286 5.65714286,7.38571429 5.97142857,7.07142857 L7.07142857,5.97142857 C7.38571429,5.65714286 7.85714286,5.65714286 8.17142857,5.97142857" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg
index dca5d289767..326ad04e017 100644..100755
--- a/app/views/shared/icons/_icon_status_created.svg
+++ b/app/views/shared/icons/_icon_status_created.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" class="ci-status-icon-created" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_created_borderless.svg b/app/views/shared/icons/_icon_status_created_borderless.svg
new file mode 100644
index 00000000000..1810d023be8
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_created_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><circle id="Oval" cx="11" cy="11" r="5.10714286"></circle></svg>
diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg
index e56e0887416..64da5aa31fc 100644..100755
--- a/app/views/shared/icons/_icon_status_failed.svg
+++ b/app/views/shared/icons/_icon_status_failed.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
- <g fill="#D22852" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <path d="M7.72916667,6.27083333 L7.72916667,4.28939247 C7.72916667,4.12531853 7.59703895,4 7.43405116,4 L6.56594884,4 C6.40541585,4 6.27083333,4.12956542 6.27083333,4.28939247 L6.27083333,6.27083333 L4.28939247,6.27083333 C4.12531853,6.27083333 4,6.40296105 4,6.56594884 L4,7.43405116 C4,7.59458415 4.12956542,7.72916667 4.28939247,7.72916667 L6.27083333,7.72916667 L6.27083333,9.71060753 C6.27083333,9.87468147 6.40296105,10 6.56594884,10 L7.43405116,10 C7.59458415,10 7.72916667,9.87043458 7.72916667,9.71060753 L7.72916667,7.72916667 L9.71060753,7.72916667 C9.87468147,7.72916667 10,7.59703895 10,7.43405116 L10,6.56594884 C10,6.40541585 9.87043458,6.27083333 9.71060753,6.27083333 L7.72916667,6.27083333 Z" transform="rotate(-45 7 7)"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_failed_borderless.svg b/app/views/shared/icons/_icon_status_failed_borderless.svg
new file mode 100644
index 00000000000..b7022350c74
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_failed_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.1458333,9.85416667 L12.1458333,6.74047388 C12.1458333,6.4826434 11.9382041,6.28571429 11.6820804,6.28571429 L10.3179196,6.28571429 C10.0656535,6.28571429 9.85416667,6.48931709 9.85416667,6.74047388 L9.85416667,9.85416667 L6.74047388,9.85416667 C6.4826434,9.85416667 6.28571429,10.0617959 6.28571429,10.3179196 L6.28571429,11.6820804 C6.28571429,11.9343465 6.48931709,12.1458333 6.74047388,12.1458333 L9.85416667,12.1458333 L9.85416667,15.2595261 C9.85416667,15.5173566 10.0617959,15.7142857 10.3179196,15.7142857 L11.6820804,15.7142857 C11.9343465,15.7142857 12.1458333,15.5106829 12.1458333,15.2595261 L12.1458333,12.1458333 L15.2595261,12.1458333 C15.5173566,12.1458333 15.7142857,11.9382041 15.7142857,11.6820804 L15.7142857,10.3179196 C15.7142857,10.0656535 15.5106829,9.85416667 15.2595261,9.85416667 L12.1458333,9.85416667 Z" id="Combined-Shape" transform="translate(11.000000, 11.000000) rotate(-45.000000) translate(-11.000000, -11.000000) "></path></svg>
diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg
new file mode 100755
index 00000000000..c98839f51a9
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_manual.svg
@@ -0,0 +1 @@
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_manual_borderless.svg b/app/views/shared/icons/_icon_status_manual_borderless.svg
new file mode 100644
index 00000000000..5eec665688b
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_manual_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M16.5,11.9906832 L16.5,10.0093168 L15.2625,9.80434783 C15.19375,9.5310559 15.05625,9.25776398 14.85,8.84782609 L15.60625,7.82298137 L14.1625,6.38819876 L13.13125,7.13975155 C12.7875,6.93478261 12.44375,6.79813665 12.16875,6.72981366 L12.03125,5.5 L10.0375,5.5 L9.83125,6.72981366 C9.4875,6.79813665 9.2125,6.93478261 8.86875,7.13975155 L7.8375,6.38819876 L6.39375,7.82298137 L7.08125,8.84782609 C6.875,9.18944099 6.80625,9.46273292 6.66875,9.80434783 L5.5,9.94099379 L5.5,11.9223602 L6.7375,12.1273292 C6.80625,12.4689441 6.94375,12.742236 7.15,13.0838509 L6.4625,14.1086957 L7.90625,15.5434783 L8.9375,14.8602484 C9.2125,14.9968944 9.55625,15.1335404 9.9,15.2701863 L10.10625,16.5 L12.16875,16.5 L12.375,15.2701863 C12.71875,15.2018634 12.99375,15.0652174 13.3375,14.8602484 L14.36875,15.6118012 L15.8125,14.1770186 L15.05625,13.1521739 C15.2625,12.810559 15.4,12.4689441 15.46875,12.1956522 L16.5,11.9906832 L16.5,11.9906832 Z M11,13.015528 C9.83125,13.015528 8.9375,12.1273292 8.9375,10.9658385 C8.9375,9.80434783 9.83125,8.91614907 11,8.91614907 C12.16875,8.91614907 13.0625,9.80434783 13.0625,10.9658385 C13.0625,12.1273292 12.16875,13.015528 11,13.015528 L11,13.015528 Z" id="Shape" ></path></svg>
diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg
index 117f0367161..02d5da407e3 100644..100755
--- a/app/views/shared/icons/_icon_status_pending.svg
+++ b/app/views/shared/icons/_icon_status_pending.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
- <g fill="#E75E40" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <path d="M4.69999981,5.30065012 C4.69999981,5.13460564 4.83842754,5 5.00354719,5 L5.89645243,5 C6.06409702,5 6.19999981,5.13308716 6.19999981,5.30065012 L6.19999981,8.69934988 C6.19999981,8.86539436 6.06157207,9 5.89645243,9 L5.00354719,9 C4.8359026,9 4.69999981,8.86691284 4.69999981,8.69934988 L4.69999981,5.30065012 Z M7.69999981,5.30065012 C7.69999981,5.13460564 7.83842754,5 8.00354719,5 L8.89645243,5 C9.06409702,5 9.19999981,5.13308716 9.19999981,5.30065012 L9.19999981,8.69934988 C9.19999981,8.86539436 9.06157207,9 8.89645243,9 L8.00354719,9 C7.8359026,9 7.69999981,8.86691284 7.69999981,8.69934988 L7.69999981,5.30065012 Z"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_pending_borderless.svg b/app/views/shared/icons/_icon_status_pending_borderless.svg
new file mode 100644
index 00000000000..8d66e9e6c9c
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_pending_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M7.38571429,8.32857143 C7.38571429,8.01428571 7.54285714,7.85714286 7.85714286,7.85714286 L9.27142857,7.85714286 C9.58571429,7.85714286 9.74285714,8.01428571 9.74285714,8.32857143 L9.74285714,13.6714286 C9.74285714,13.9857143 9.58571429,14.1428571 9.27142857,14.1428571 L7.85714286,14.1428571 C7.54285714,14.1428571 7.38571429,13.9857143 7.38571429,13.6714286 L7.38571429,8.32857143 M12.1,8.32857143 C12.1,8.01428571 12.2571429,7.85714286 12.5714286,7.85714286 L13.9857143,7.85714286 C14.3,7.85714286 14.4571429,8.01428571 14.4571429,8.32857143 L14.4571429,13.6714286 C14.4571429,13.9857143 14.3,14.1428571 13.9857143,14.1428571 L12.5714286,14.1428571 C12.2571429,14.1428571 12.1,13.9857143 12.1,13.6714286 L12.1,8.32857143" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg
index 920d7952eb5..532f4fee33c 100644..100755
--- a/app/views/shared/icons/_icon_status_running.svg
+++ b/app/views/shared/icons/_icon_status_running.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
- <g fill="#2D9FD8" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <path d="M7,3 C9.209139,3 11,4.790861 11,7 C11,9.209139 9.209139,11 7,11 C5.65802855,11 4.47040669,10.3391508 3.74481446,9.32513253 L7,7 L7,3 L7,3 Z"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_running_borderless.svg b/app/views/shared/icons/_icon_status_running_borderless.svg
new file mode 100644
index 00000000000..2757a168ed5
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_running_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11,4.71428571 C14.4571429,4.71428571 17.2857143,7.54285714 17.2857143,11 C17.2857143,14.4571429 14.4571429,17.2857143 11,17.2857143 C8.95714286,17.2857143 7.07142857,16.1857143 5.81428571,14.6142857 L11,11 L11,4.71428571" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg
index 014ca86b61b..1998dfef9ea 100644..100755
--- a/app/views/shared/icons/_icon_status_skipped.svg
+++ b/app/views/shared/icons/_icon_status_skipped.svg
@@ -1 +1 @@
-<svg width="20" height="20" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_skipped_borderless.svg b/app/views/shared/icons/_icon_status_skipped_borderless.svg
new file mode 100644
index 00000000000..fb3e930b3cb
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_skipped_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.0846,12.1 L10.6623,13.5223 C10.2454306,13.9539168 10.2513924,14.6399933 10.6756996,15.0643004 C11.1000067,15.4886076 11.7860832,15.4945694 12.2177,15.0777 L15.1261,12.1693 C15.7708612,11.5230891 15.7708612,10.4769109 15.1261,9.8307 L12.2177,6.9223 C11.7860832,6.50543057 11.1000067,6.51139239 10.6756996,6.93569957 C10.2513924,7.36000675 10.2454306,8.04608322 10.6623,8.4777 L12.0846,9.9 L7.04,9.9 C6.43248678,9.9 5.94,10.3924868 5.94,11 C5.94,11.6075132 6.43248678,12.1 7.04,12.1 L12.0846,12.1 L12.0846,12.1 Z" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg
index 67b378b3571..eed5006bebe 100644..100755
--- a/app/views/shared/icons/_icon_status_success.svg
+++ b/app/views/shared/icons/_icon_status_success.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
- <g fill="#31AF64" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <path d="M7.29166667,7.875 L5.54840803,7.875 C5.38293028,7.875 5.25,8.00712771 5.25,8.17011551 L5.25,9.03821782 C5.25,9.19875081 5.38360183,9.33333333 5.54840803,9.33333333 L8.24853534,9.33333333 C8.52035522,9.33333333 8.75,9.11228506 8.75,8.83960819 L8.75,8.46475969 L8.75,4.07392947 C8.75,3.92144267 8.61787229,3.79166667 8.45488449,3.79166667 L7.58678218,3.79166667 C7.42624919,3.79166667 7.29166667,3.91804003 7.29166667,4.07392947 L7.29166667,7.875 Z" transform="rotate(45 7 6.563)"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_success_borderless.svg b/app/views/shared/icons/_icon_status_success_borderless.svg
new file mode 100644
index 00000000000..8ee5be7ab78
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_success_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.4583333,12.375 L8.70008808,12.375 C8.45889044,12.375 8.25,12.5826293 8.25,12.8387529 L8.25,14.2029137 C8.25,14.4551799 8.4515113,14.6666667 8.70008808,14.6666667 L12.9619841,14.6666667 C13.3891296,14.6666667 13.75,14.3193051 13.75,13.8908129 L13.75,13.2899463 L13.75,6.42552703 C13.75,6.16226705 13.5423707,5.95833333 13.2862471,5.95833333 L11.9220863,5.95833333 C11.6698201,5.95833333 11.4583333,6.16750307 11.4583333,6.42552703 L11.4583333,12.375 Z" id="Combined-Shape" transform="translate(11.000000, 10.312500) rotate(-315.000000) translate(-11.000000, -10.312500) "></path></svg>
diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg
index d0ad4bd65b1..cb785635b7e 100644..100755
--- a/app/views/shared/icons/_icon_status_warning.svg
+++ b/app/views/shared/icons/_icon_status_warning.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
- <g fill="#FF8A24" fill-rule="evenodd">
- <path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
- <path d="M6,3.49769878 C6,3.22282734 6.21403503,3 6.50468445,3 L7.49531555,3 C7.77404508,3 8,3.21484375 8,3.49769878 L8,7.50230122 C8,7.77717266 7.78596497,8 7.49531555,8 L6.50468445,8 C6.22595492,8 6,7.78515625 6,7.50230122 L6,3.49769878 Z M6,9.50468445 C6,9.22595492 6.21403503,9 6.50468445,9 L7.49531555,9 C7.77404508,9 8,9.21403503 8,9.50468445 L8,10.4953156 C8,10.7740451 7.78596497,11 7.49531555,11 L6.50468445,11 C6.22595492,11 6,10.785965 6,10.4953156 L6,9.50468445 Z"/>
- </g>
-</svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_warning_borderless.svg b/app/views/shared/icons/_icon_status_warning_borderless.svg
new file mode 100644
index 00000000000..7b061624521
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_warning_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M9.42857143,5.5 C9.42857143,5.02857143 9.74285714,4.71428571 10.2142857,4.71428571 L11.7857143,4.71428571 C12.2571429,4.71428571 12.5714286,5.02857143 12.5714286,5.5 L12.5714286,11.7857143 C12.5714286,12.2571429 12.2571429,12.5714286 11.7857143,12.5714286 L10.2142857,12.5714286 C9.74285714,12.5714286 9.42857143,12.2571429 9.42857143,11.7857143 L9.42857143,5.5 M9.42857143,14.9285714 C9.42857143,14.4571429 9.74285714,14.1428571 10.2142857,14.1428571 L11.7857143,14.1428571 C12.2571429,14.1428571 12.5714286,14.4571429 12.5714286,14.9285714 L12.5714286,16.5 C12.5714286,16.9714286 12.2571429,17.2857143 11.7857143,17.2857143 L10.2142857,17.2857143 C9.74285714,17.2857143 9.42857143,16.9714286 9.42857143,16.5 L9.42857143,14.9285714" id="Shape"></path></svg>
diff --git a/app/views/shared/icons/_icon_stopwatch.svg b/app/views/shared/icons/_icon_stopwatch.svg
new file mode 100644
index 00000000000..f20de04538e
--- /dev/null
+++ b/app/views/shared/icons/_icon_stopwatch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_icon_terminal.svg b/app/views/shared/icons/_icon_terminal.svg
new file mode 100644
index 00000000000..c80f44c3edf
--- /dev/null
+++ b/app/views/shared/icons/_icon_terminal.svg
@@ -0,0 +1 @@
+<svg width="19" height="14" viewBox="0 0 19 14" xmlns="http://www.w3.org/2000/svg"><rect fill="#848484" x="7.2" y="9.25" width="6.46" height="1.5" rx=".5"/><path d="M5.851 7.016L3.81 9.103a.503.503 0 0 0 .017.709l.35.334c.207.198.524.191.717-.006l2.687-2.748a.493.493 0 0 0 .137-.376.493.493 0 0 0-.137-.376L4.894 3.892a.507.507 0 0 0-.717-.006l-.35.334a.503.503 0 0 0-.017.709L5.85 7.016z"/><path d="M1.25 11.497c0 .691.562 1.253 1.253 1.253h13.994c.694 0 1.253-.56 1.253-1.253V2.503c0-.691-.562-1.253-1.253-1.253H2.503c-.694 0-1.253.56-1.253 1.253v8.994zM2.503 0h13.994A2.504 2.504 0 0 1 19 2.503v8.994A2.501 2.501 0 0 1 16.497 14H2.503A2.504 2.504 0 0 1 0 11.497V2.503A2.501 2.501 0 0 1 2.503 0z"/></svg>
diff --git a/app/views/shared/icons/_mattermost_logo.svg.erb b/app/views/shared/icons/_mattermost_logo.svg.erb
new file mode 100644
index 00000000000..83fbd1a407d
--- /dev/null
+++ b/app/views/shared/icons/_mattermost_logo.svg.erb
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg>
diff --git a/app/views/shared/icons/_scroll_down.svg b/app/views/shared/icons/_scroll_down.svg
new file mode 100644
index 00000000000..acf22ac9314
--- /dev/null
+++ b/app/views/shared/icons/_scroll_down.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-down" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M1.385 5.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15V5.535a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_down_hover_active.svg b/app/views/shared/icons/_scroll_down_hover_active.svg
new file mode 100644
index 00000000000..262576acf54
--- /dev/null
+++ b/app/views/shared/icons/_scroll_down_hover_active.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-down-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_up.svg b/app/views/shared/icons/_scroll_up.svg
new file mode 100644
index 00000000000..f11288fd59c
--- /dev/null
+++ b/app/views/shared/icons/_scroll_up.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-up" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M1.385 14.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15v-12.47a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 2.609V6.96a.688.688 0 0 1-.69.688.687.687 0 0 1-.69-.688V2.627L6.155 3.972a.69.69 0 0 1-.976-.976L7.705.47a.685.685 0 0 1 .494-.2.685.685 0 0 1 .493.2l2.526 2.526a.69.69 0 1 1-.976.976L8.88 2.609zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_up_hover_active.svg b/app/views/shared/icons/_scroll_up_hover_active.svg
new file mode 100644
index 00000000000..4658dbb1bb7
--- /dev/null
+++ b/app/views/shared/icons/_scroll_up_hover_active.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-up-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M8.88 2.646l1.362 1.362a.69.69 0 0 0 .976-.976L8.692.507A.685.685 0 0 0 8.2.306a.685.685 0 0 0-.494.2L5.179 3.033a.69.69 0 1 0 .976.976L7.5 2.663v4.179c0 .38.306.688.69.688.381 0 .69-.306.69-.688V2.646zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index ed93857e6d4..b42eaabb111 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -29,20 +29,20 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
- - if issuable_filters_present
+ - if issuable_filter_present?
.filter-item.inline.reset-filters
- %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters
+ %a{ href: page_filter_path(without: issuable_filter_params) } Reset filters
.pull-right
- if boards_page
- #js-boards-seach.issue-boards-search
+ #js-boards-search.issue-boards-search
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
.dropdown.pull-right
%button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
- Create new list
+ Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }
+ = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- if can?(current_user, :admin_label, @project)
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
@@ -56,9 +56,9 @@
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul
%li
- %a{href: "#", data: {id: "reopen"}} Open
+ %a{ href: "#", data: { id: "reopen" } } Open
%li
- %a{href: "#", data: {id: "close"}} Closed
+ %a{ href: "#", data: {id: "close" } } Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
@@ -70,9 +70,9 @@
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul
%li
- %a{href: "#", data: {id: "subscribe"}} Subscribe
+ %a{ href: "#", data: { id: "subscribe" } } Subscribe
%li
- %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe
+ %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 3176af9c19b..c0e8a498316 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,3 +1,5 @@
+- form = local_assigns.fetch(:f)
+- commits = local_assigns[:commits]
- project = @target_project || @project
= form_errors(issuable)
@@ -10,91 +12,22 @@
and make sure your changes will not unintentionally remove theirs
.form-group
- = f.label :title, class: 'control-label'
+ = form.label :title, class: 'control-label'
= render 'shared/issuable/form/template_selector', issuable: issuable
+ = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
- %div{ class: issuable_templates(issuable).any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
- = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
- class: 'form-control pad', required: true
+= render 'shared/issuable/form/description', issuable: issuable, form: form
- - if issuable.is_a?(MergeRequest)
- %p.help-block
- .js-wip-explanation
- %a.js-toggle-wip{href: "", tabindex: -1}
- Remove the
- %code WIP:
- prefix from the title
- to allow this
- %strong Work In Progress
- merge request to be merged when it's ready.
- .js-no-wip-explanation
- %a.js-toggle-wip{href: "", tabindex: -1}
- Start the title with
- %code WIP:
- to prevent a
- %strong Work In Progress
- merge request from being merged before it's ready.
-
- - if can_add_template?(issuable)
- %p.help-block
- Add
- = link_to "description templates", help_page_path('user/project/description_templates'), tabindex: -1
- to help your contributors communicate effectively!
-
-.form-group.detail-page-description
- = f.label :description, 'Description', class: 'control-label'
- .col-sm-10
-
- = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description,
- classes: 'note-textarea',
- placeholder: "Write a comment or drag your files here...",
- supports_slash_commands: !issuable.persisted?
- = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted?
- .clearfix
- .error-alert
-
-- if issuable.is_a?(Issue)
+- if issuable.respond_to?(:confidential)
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
- = f.label :confidential do
- = f.check_box :confidential
+ = form.label :confidential do
+ = form.check_box :confidential
This issue is confidential and should only be visible to team members with at least Reporter access.
-- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
- - has_due_date = issuable.has_attribute?(:due_date)
- %hr
- .row
- %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
- .form-group.issue-assignee
- = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
- .col-sm-10{ class: ("col-lg-8" if has_due_date) }
- .issuable-form-select-holder
- - if issuable.assignee_id
- = f.hidden_field :assignee_id
- = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
- .form-group.issue-milestone
- = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
- .col-sm-10{ class: ("col-lg-8" if has_due_date) }
- .issuable-form-select-holder
- = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
- .form-group
- - has_labels = @labels && @labels.any?
- = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
- = f.hidden_field :label_ids, multiple: true, value: ''
- .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
- .issuable-form-select-holder
- = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
- - if has_due_date
- .col-lg-6
- .form-group
- = f.label :due_date, "Due date", class: "control-label"
- .col-sm-10
- .issuable-form-select-holder
- = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
+= render 'shared/issuable/form/metadata', issuable: issuable, form: form
- if issuable.can_move?(current_user)
%hr
@@ -108,37 +41,29 @@
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
= icon('question-circle')
-- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork?
- %hr
- - if @merge_request.new_record?
- .form-group
- = f.label :source_branch, class: 'control-label'
- .col-sm-10
- .issuable-form-select-holder
- = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
+= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
+
+- if @merge_request_for_resolving_discussions
.form-group
- = f.label :target_branch, class: 'control-label'
- .col-sm-10
- .issuable-form-select-holder
- = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
- - if @merge_request.new_record?
- &nbsp;
- = link_to 'Change branches', mr_change_branches_path(@merge_request)
- - if @merge_request.can_remove_source_branch?(current_user)
- .form-group
- .col-sm-10.col-sm-offset-2
- .checkbox
- = label_tag 'merge_request[force_remove_source_branch]' do
- = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
- = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch?
- Remove source branch when merge request is accepted.
+ .col-sm-10.col-sm-offset-2
+ - if @merge_request_for_resolving_discussions.discussions_can_be_resolved_by?(current_user)
+ = icon('exclamation-triangle')
+ Creating this issue will mark all discussions in
+ = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions)
+ as resolved.
+ = hidden_field_tag 'merge_request_for_resolving_discussions', @merge_request_for_resolving_discussions.iid
+ - else
+ = icon('exclamation-triangle')
+ You can't automatically mark all discussions in
+ = link_to @merge_request_for_resolving_discussions.to_reference, merge_request_path(@merge_request_for_resolving_discussions)
+ as resolved. Ask someone with sufficient rights to resolve the them.
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
-.row-content-block{class: (is_footer ? "footer-block" : "middle-block")}
+.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
- if issuable.new_record?
- = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
+ = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
- else
- = f.submit 'Save changes', class: 'btn btn-save'
+ = form.submit 'Save changes', class: 'btn btn-save'
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
.inline.prepend-left-10
@@ -151,8 +76,8 @@
- else
.pull-right
- if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
- = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
+ = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" },
method: :delete, class: 'btn btn-danger btn-grouped'
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
-= f.hidden_field :lock_version
+= form.hidden_field :lock_version
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 1d778bc88de..93c7fa0c7d6 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -19,10 +19,10 @@
= hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data }
%span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
= multi_label_name(selected, "Labels")
- = icon('caret-down')
+ = icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
- if show_create && project && can?(current_user, :admin_label, project)
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index 3bc57d3d2ac..bd66f39fa59 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -9,7 +9,7 @@
&nbsp
.dropdown-label-color-input
.dropdown-label-color-preview.js-dropdown-label-color-preview
- %input#new_label_color.default-dropdown-input{ type: "text" }
+ %input#new_label_color.default-dropdown-input{ type: "text", placeholder: "Assign custom color like #FF0000" }
.clearfix
%button.btn.btn-primary.pull-left.js-new-label-btn{ type: "button" }
Create
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index c0dc63be2bf..9a8529c6cbb 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -1,17 +1,15 @@
- title = local_assigns.fetch(:title, 'Assign labels')
- show_create = local_assigns.fetch(:show_create, true)
- show_footer = local_assigns.fetch(:show_footer, true)
-- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels')
+- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search')
- show_boards_content = local_assigns.fetch(:show_boards_content, false)
.dropdown-page-one
= dropdown_title(title)
- if show_boards_content
.issue-board-dropdown-content
%p
- Each label that exists in your issue tracker can have its own dedicated
- list. Select a label below to add a list to your Board and it will
- automatically be populated with issues that have that label. To create
- a list for a label that doesn't exist yet, simply create the label below.
+ Create lists from the labels you use in your project. Issues with that
+ label will automatically be added to the list.
= dropdown_filter(filter_placeholder)
= dropdown_content
- if @project && show_footer
@@ -19,7 +17,7 @@
%ul.dropdown-footer-list
- if can?(current_user, :admin_label, @project)
%li
- %a.dropdown-toggle-page{href: "#"}
+ %a.dropdown-toggle-page{ href: "#" }
Create new label
%li
= link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 40fe53e6a8d..415361f8fbf 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -3,7 +3,7 @@
- show_menu_above = show_menu_above || false
- selected_text = selected.try(:title) || params[:milestone_title]
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
-- if selected.present?
+- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 5527a2f889a..1154316c03f 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -3,23 +3,23 @@
- issuables = @issues || @merge_requests
%ul.nav-links.issues-state-filters
- %li{class: ("active" if params[:state] == 'opened')}
- = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
+ %li{ class: ("active" if params[:state] == 'opened') }>
+ = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do
#{issuables_state_counter_text(type, :opened)}
- if type == :merge_requests
- %li{class: ("active" if params[:state] == 'merged')}
- = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
+ %li{ class: ("active" if params[:state] == 'merged') }>
+ = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do
#{issuables_state_counter_text(type, :merged)}
- %li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
+ %li{ class: ("active" if params[:state] == 'closed') }>
+ = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do
#{issuables_state_counter_text(type, :closed)}
- else
- %li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
+ %li{ class: ("active" if params[:state] == 'closed') }>
+ = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do
#{issuables_state_counter_text(type, :closed)}
- %li{class: ("active" if params[:state] == 'all')}
- = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
+ %li{ class: ("active" if params[:state] == 'all') }>
+ = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do
#{issuables_state_counter_text(type, :all)}
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index 33a9a494857..171da899937 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -13,8 +13,8 @@
.participants-author.js-participants-author
= link_to_member(@project, participant, name: false, size: 24)
- if participants_extra > 0
- %div.participants-more
- %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}}
+ .participants-more
+ %a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
+ #{participants_extra} more
:javascript
IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
new file mode 100644
index 00000000000..e9644ca0f12
--- /dev/null
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -0,0 +1,137 @@
+- type = local_assigns.fetch(:type)
+
+.issues-filters
+ .issues-details-filters.row-content-block.second-block.filtered-search-block
+ = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
+ - if params[:search].present?
+ = hidden_field_tag :search, params[:search]
+ - if @bulk_edit
+ .check-all-holder
+ = check_box_tag "check_all_issues", nil, false,
+ class: "check_all_issues left"
+ .issues-other-filters.filtered-search-container
+ .filtered-search-input-container
+ %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]) }
+ = icon('filter')
+ %button.clear-search.hidden{ type: 'button' }
+ = icon('times')
+ #js-dropdown-hint.dropdown-menu.hint-dropdown
+ %ul{ 'data-dropdown' => true }
+ %li.filter-dropdown-item{ 'data-action' => 'submit' }
+ %button.btn.btn-link
+ = icon('search')
+ %span
+ Keep typing and press Enter
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link
+ -# Encapsulate static class name `{{icon}}` inside #{} to bypass
+ -# haml lint's ClassAttributeWithStaticValue
+ %i.fa{ class: "#{'{{icon}}'}" }
+ %span.js-filter-hint
+ {{hint}}
+ %span.js-filter-tag.dropdown-light-content
+ {{tag}}
+ #js-dropdown-author.dropdown-menu
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.dropdown-user
+ %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+ .dropdown-user-details
+ %span
+ {{name}}
+ %span.dropdown-light-content
+ @{{username}}
+ #js-dropdown-assignee.dropdown-menu
+ %ul{ 'data-dropdown' => true }
+ %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %button.btn.btn-link
+ No Assignee
+ - if current_user
+ %li.filter-dropdown-item{ 'data-value' => current_user.to_reference }
+ %button.btn.btn-link
+ Assigned to me
+ %li.divider
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.dropdown-user
+ %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+ .dropdown-user-details
+ %span
+ {{name}}
+ %span.dropdown-light-content
+ @{{username}}
+ #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true }
+ %ul{ 'data-dropdown' => true }
+ %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %button.btn.btn-link
+ No Milestone
+ %li.filter-dropdown-item{ 'data-value' => 'upcoming' }
+ %button.btn.btn-link
+ Upcoming
+ %li.divider
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value
+ {{title}}
+ #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true }
+ %ul{ 'data-dropdown' => true }
+ %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %button.btn.btn-link
+ No Label
+ %li.divider
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link
+ %span.dropdown-label-box{ style: 'background: {{color}}' }
+ %span.label-title.js-data-value
+ {{title}}
+ .pull-right
+ = render 'shared/sort_dropdown'
+
+ - if @bulk_edit
+ .issues_bulk_update.hide
+ = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
+ .filter-item.inline
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ %ul
+ %li
+ %a{ href: "#", data: { id: "reopen" } } Open
+ %li
+ %a{ href: "#", data: { id: "close" } } Closed
+ .filter-item.inline
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
+ .filter-item.inline
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
+ .filter-item.inline.labels-filter
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ .filter-item.inline
+ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
+ %ul
+ %li
+ %a{ href: "#", data: { id: "subscribe" } } Subscribe
+ %li
+ %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
+
+ = hidden_field_tag 'update[issuable_ids]', []
+ = hidden_field_tag :state_event, params[:state_event]
+ .filter-item.inline
+ = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
+
+:javascript
+ new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
+ new SubscriptionSelect();
+
+ $(document).off('page:restore').on('page:restore', function (event) {
+ if (gl.FilteredSearchManager) {
+ new gl.FilteredSearchManager();
+ }
+ Issuable.init();
+ new gl.IssuableBulkActions({
+ prefixId: 'issue_',
+ });
+ });
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 7363ead09ff..ec9bcaf63dd 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,5 +1,7 @@
- todo = issuable_todo(issuable)
-%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('issuable/issuable_bundle.js')
+%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
@@ -9,17 +11,17 @@
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
= sidebar_gutter_toggle_icon
- if current_user
- %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
+ %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add todo" : "Mark done") }, data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
%span.js-issuable-todo-text
- if todo
- Mark Done
+ Mark done
- else
- Add Todo
+ Add todo
= icon('spin spinner', class: 'hidden js-issuable-todo-loading')
- = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
+ = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
.block.assignee
- .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.name if issuable.assignee)}
+ .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24)
- else
@@ -54,7 +56,7 @@
= icon('clock-o')
%span
- if issuable.milestone
- %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1, placement: 'left'}}
+ %span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
= issuable.milestone.title
- else
None
@@ -72,7 +74,13 @@
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
= dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
-
+ - if issuable.has_attribute?(:time_estimate)
+ #issuable-time-tracker.block
+ %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') }
+ // Fallback while content is loading
+ .title.hide-collapsed
+ Time tracking
+ = icon('spinner spin')
- if issuable.has_attribute?(:due_date)
.block.due_date
.sidebar-collapsed-icon
@@ -97,7 +105,7 @@
remove due date
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.selectbox.hide-collapsed
- = f.hidden_field :due_date, value: issuable.due_date
+ = f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd')
.dropdown
%button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } }
%span.dropdown-toggle-text Due date
@@ -129,8 +137,8 @@
- selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
- %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?)}
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } }
+ %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
@@ -140,36 +148,33 @@
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user
- - subscribed = issuable.subscribed?(current_user)
- .block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
+ - subscribed = issuable.subscribed?(current_user, @project)
+ .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } }
.sidebar-collapsed-icon
= icon('rss')
- .title.hide-collapsed
+ %span.issuable-header-text.hide-collapsed.pull-left
Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
- .subscription-status.hide-collapsed{data: {status: subscribtion_status}}
- .unsubscribed{class: ( 'hidden' if subscribed )}
- You're not receiving notifications from this thread.
- .subscribed{class: ( 'hidden' unless subscribed )}
- You're receiving notifications because you're subscribed to this thread.
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
.sidebar-collapsed-icon.dont-change-state
- = clipboard_button(clipboard_text: project_ref)
+ = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left")
.cross-project-reference.hide-collapsed
%span
Reference:
- %cite{title: project_ref}
+ %cite{ title: project_ref }
= project_ref
- = clipboard_button(clipboard_text: project_ref)
+ = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left")
:javascript
+ gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}');
+ new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}");
new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
new LabelsSelect();
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
- new Subscription('.subscription')
+ gl.Subscription.bindAll('.subscription');
new gl.DueDateSelectors();
- sidebar = new Sidebar();
+ window.sidebar = new Sidebar();
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
new file mode 100644
index 00000000000..b757893ea04
--- /dev/null
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -0,0 +1,30 @@
+- issuable = local_assigns.fetch(:issuable)
+- form = local_assigns.fetch(:form)
+
+- return unless issuable.is_a?(MergeRequest)
+- return if issuable.closed_without_fork?
+
+%hr
+- if issuable.new_record?
+ .form-group
+ = form.label :source_branch, class: 'control-label'
+ .col-sm-10
+ .issuable-form-select-holder
+ = form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2 span2', disabled: true })
+.form-group
+ = form.label :target_branch, class: 'control-label'
+ .col-sm-10
+ .issuable-form-select-holder
+ = form.select(:target_branch, issuable.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: issuable.new_record?, data: { placeholder: "Select branch" }})
+ - if issuable.new_record?
+ &nbsp;
+ = link_to 'Change branches', mr_change_branches_path(issuable)
+
+- if issuable.can_remove_source_branch?(current_user)
+ .form-group
+ .col-sm-10.col-sm-offset-2
+ .checkbox
+ = label_tag 'merge_request[force_remove_source_branch]' do
+ = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
+ = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
+ Remove source branch when merge request is accepted.
diff --git a/app/views/shared/issuable/form/_description.html.haml b/app/views/shared/issuable/form/_description.html.haml
new file mode 100644
index 00000000000..dbace9ce401
--- /dev/null
+++ b/app/views/shared/issuable/form/_description.html.haml
@@ -0,0 +1,15 @@
+- issuable = local_assigns.fetch(:issuable)
+- form = local_assigns.fetch(:form)
+
+.form-group.detail-page-description
+ = form.label :description, 'Description', class: 'control-label'
+ .col-sm-10
+
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
+ = render 'projects/zen', f: form, attr: :description,
+ classes: 'note-textarea',
+ placeholder: "Write a comment or drag your files here...",
+ supports_slash_commands: !issuable.persisted?
+ = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted?
+ .clearfix
+ .error-alert
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
new file mode 100644
index 00000000000..a47085230b8
--- /dev/null
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -0,0 +1,38 @@
+- issuable = local_assigns.fetch(:issuable)
+
+- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+
+- has_due_date = issuable.has_attribute?(:due_date)
+- has_labels = @labels && @labels.any?
+- form = local_assigns.fetch(:form)
+
+%hr
+.row
+ %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
+ .form-group.issue-assignee
+ = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ - if issuable.assignee_id
+ = form.hidden_field :assignee_id
+ = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
+ .form-group.issue-milestone
+ = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ .form-group
+ - has_labels = @labels && @labels.any?
+ = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.hidden_field :label_ids, multiple: true, value: ''
+ .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ .issuable-form-select-holder
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
+ - if has_due_date
+ .col-lg-6
+ .form-group
+ = form.label :due_date, "Due date", class: "control-label"
+ .col-sm-10
+ .issuable-form-select-holder
+ = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
new file mode 100644
index 00000000000..64826d41d60
--- /dev/null
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -0,0 +1,36 @@
+- issuable = local_assigns.fetch(:issuable)
+- has_wip_commits = local_assigns.fetch(:has_wip_commits)
+- form = local_assigns.fetch(:form)
+- no_issuable_templates = issuable_templates(issuable).empty?
+- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
+
+%div{ class: div_class }
+ = form.text_field :title, required: true, maxlength: 255, autofocus: true,
+ autocomplete: 'off', class: 'form-control pad'
+
+ - if issuable.respond_to?(:work_in_progress?)
+ %p.help-block
+ .js-wip-explanation
+ %a.js-toggle-wip{ href: '', tabindex: -1 }
+ Remove the
+ %code WIP:
+ prefix from the title
+ to allow this
+ %strong Work In Progress
+ merge request to be merged when it's ready.
+ .js-no-wip-explanation
+ - if has_wip_commits
+ It looks like you have some WIP commits in this branch.
+ %br
+ %a.js-toggle-wip{ href: '', tabindex: -1 }
+ Start the title with
+ %code WIP:
+ to prevent a
+ %strong Work In Progress
+ merge request from being merged before it's ready.
+
+ - if no_issuable_templates && can?(current_user, :push_code, issuable.project)
+ %p.help-block
+ Add
+ = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1
+ to help your contributors communicate effectively!
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
index eff914398bb..fb795ad1c72 100644
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -1,10 +1,17 @@
-- if can?(current_user, :request_access, source)
- - if requester = source.requesters.find_by(user_id: current_user.id)
+- model_name = source.model_name.to_s.downcase
+
+.project-action-button.inline
+ - if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
+ = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: leave_confirmation_message(source) },
+ class: 'btn'
+ - elsif requester = source.requesters.find_by(user_id: current_user.id)
= link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: remove_member_message(requester) },
class: 'btn'
- - else
+ - elsif source.request_access_enabled && can?(current_user, :request_access, source)
= link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
method: :post,
class: 'btn'
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 1c0346bbc78..81b5bc1de30 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -1,8 +1,9 @@
- group_link = local_assigns[:group_link]
- group = group_link.group
- can_admin_member = can?(current_user, :admin_project_member, @project)
-%li.member.group_member{ id: "group_member_#{group_link.id}" }
- %span{ class: "list-item-name" }
+- dom_id = "group_member_#{group_link.id}"
+%li.member.group_member{ id: dom_id }
+ %span.list-item-name
= image_tag group_icon(group), class: "avatar s40", alt: ''
%strong
= link_to group.name, group_path(group)
@@ -14,13 +15,28 @@
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls
= form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
- = select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member
+ = hidden_field_tag "group_link[group_access]", group_link.group_access
+ .member-form-control.dropdown.append-right-5
+ %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: !can_admin_member,
+ data: { toggle: "dropdown", field_name: "group_link[group_access]" } }
+ %span.dropdown-toggle-text
+ = group_link.human_access
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ = dropdown_title("Change permissions")
+ .dropdown-content
+ %ul
+ - Gitlab::Access.options.each do |role, role_id|
+ %li
+ = link_to role, "javascript:void(0)",
+ class: ("is-active" if group_link.group_access == role_id),
+ data: { id: role_id, el_id: dom_id }
.prepend-left-5.clearable-input.member-form-control
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input
- if can_admin_member
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link),
- remote: true,
method: :delete,
data: { confirm: "Are you sure you want to remove #{group.name}?" },
class: 'btn btn-remove prepend-left-10' do
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 432047a1c4e..659d4c905fc 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -20,8 +20,8 @@
%strong Blocked
- if source.instance_of?(Group) && !@group
- = link_to source, class: "member-group-link prepend-left-5" do
- = "· #{source.name}"
+ &middot;
+ = link_to source.name, source, class: "member-group-link"
.hidden-xs.cgray
- if member.request?
@@ -45,12 +45,28 @@
= time_ago_with_tooltip(member.created_at)
- if show_roles
.controls.member-controls
- - if show_controls
+ - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project)
- if user != current_user
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
- = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member
+ = f.hidden_field :access_level
+ .member-form-control.dropdown.append-right-5
+ %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: !can_admin_member,
+ data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
+ %span.dropdown-toggle-text
+ = member.human_access
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ = dropdown_title("Change permissions")
+ .dropdown-content
+ %ul
+ - Gitlab::Access.options.each do |role, role_id|
+ %li
+ = link_to role, "javascript:void(0)",
+ class: ("is-active" if member.access_level == role_id),
+ data: { id: role_id, el_id: dom_id(member) }
.prepend-left-5.clearable-input.member-form-control
- = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member
+ = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) }
%i.clear-icon.js-clear-input
- else
%span.member-access-text= member.human_access
diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml
new file mode 100644
index 00000000000..bad0891f9f2
--- /dev/null
+++ b/app/views/shared/members/_sort_dropdown.html.haml
@@ -0,0 +1,9 @@
+.dropdown.inline.member-sort-dropdown
+ = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Sort by
+ - member_sort_options_hash.each do |value, title|
+ %li
+ = link_to filter_group_project_member_path(sort: value), class: ("is-active" if @sort == value) do
+ = title
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
new file mode 100644
index 00000000000..748b10a1298
--- /dev/null
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -0,0 +1,15 @@
+.col-md-6
+ .form-group
+ = f.label :start_date, "Start Date", class: "control-label"
+ .col-sm-10
+ = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date"
+ %a.inline.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
+.col-md-6
+ .form-group
+ = f.label :due_date, "Due Date", class: "control-label"
+ .col-sm-10
+ = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
+ %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
+
+:javascript
+ new gl.DueDateSelectors();
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 9e1b0379428..28935c8b598 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -14,15 +14,15 @@
- if issuable.is_a?(Issue)
= confidential_icon(issuable)
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
- %div{class: 'issuable-detail'}
+ .issuable-detail
= link_to [project.namespace.becomes(Namespace), project, issuable] do
- %span{ class: 'issuable-number' }>= issuable.to_reference
+ %span.issuable-number= issuable.to_reference
- issuable.labels.each do |label|
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
- render_colored_label(label)
- %span{ class: "assignee-icon" }
+ %span.assignee-icon
- if assignee
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index 8619939dde7..31eb07ca666 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -3,10 +3,12 @@
- panel_class = primary ? 'panel-primary' : 'panel-default'
.panel{ class: panel_class }
- .panel-heading
- = title
+ .panel-heading.split
+ .left
+ = title
- if show_counter
- .pull-right= issuables.size
+ .right
+ = number_with_delimiter(issuables.size)
- class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 3dccfb147bf..9e6a76e1ddb 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -1,7 +1,7 @@
- dashboard = local_assigns[:dashboard]
- custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first)
-%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
+%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
.row
.col-sm-6
%strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path
@@ -9,7 +9,7 @@
.pull-right.light #{milestone.percent_complete(current_user)}% complete
.row
.col-sm-6
- = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path
+ = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index dee2472fa79..d27fba805a3 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -3,32 +3,40 @@
.context.prepend-top-default
.milestone-summary
%h4 Progress
- %strong= milestone.issues_visible_to_user(current_user).size
- issues:
- %span.milestone-stat
- %strong= milestone.issues_visible_to_user(current_user).opened.size
- open and
- %strong= milestone.issues_visible_to_user(current_user).closed.size
- closed
- %strong= milestone.merge_requests.size
- merge requests:
- %span.milestone-stat
- %strong= milestone.merge_requests.opened.size
- open and
- %strong= milestone.merge_requests.merged.size
- merged
- %span.milestone-stat
- %strong== #{milestone.percent_complete(current_user)}%
- complete
- %span.milestone-stat
- %span.remaining-days= milestone_remaining_days(milestone)
- %span.pull-right.tab-issues-buttons
- - if project && can?(current_user, :create_issue, project)
- = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do
- New Issue
- = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped"
- %span.pull-right.tab-merge-requests-buttons.hidden
- = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped"
+ .milestone-stats-and-buttons
+ .milestone-stats
+ %span.milestone-stat.with-drilldown
+ %strong= milestone.issues_visible_to_user(current_user).size
+ issues:
+ %span.milestone-stat
+ %strong= milestone.issues_visible_to_user(current_user).opened.size
+ open and
+ %strong= milestone.issues_visible_to_user(current_user).closed.size
+ closed
+ %span.milestone-stat.with-drilldown
+ %strong= milestone.merge_requests.size
+ merge requests:
+ %span.milestone-stat
+ %strong= milestone.merge_requests.opened.size
+ open and
+ %strong= milestone.merge_requests.merged.size
+ merged
+ %span.milestone-stat
+ %strong== #{milestone.percent_complete(current_user)}%
+ complete
+ - remaining_days = milestone_remaining_days(milestone)
+ - if remaining_days.present?
+ %span.milestone-stat
+ %span.remaining-days= remaining_days
- = milestone_progress_bar(milestone)
+ .milestone-progress-buttons
+ %span.tab-issues-buttons
+ - if project && can?(current_user, :create_issue, project)
+ = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
+ New Issue
+ = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
+ %span.tab-merge-requests-buttons.hidden
+ = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn"
+
+ = milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index 2b6ce2d7e7a..c8f2319d95a 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -21,7 +21,7 @@
.tab-content.milestone-content
.tab-pane.active#tab-issues
- = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-participants
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 548215243db..497446c1ef3 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -12,10 +12,10 @@
Open
%span.identifier
Milestone #{milestone.title}
- - if milestone.expires_at
+ - if milestone.due_date || milestone.start_date
%span.creator
&middot;
- = milestone.expires_at
+ = milestone_date_range(milestone)
- if group
.pull-right
- if can?(current_user, :admin_milestones, group)
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index feaa5570c21..1d072c16b32 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,6 +1,5 @@
-- left_align = local_assigns[:left_align]
- if notification_setting
- .dropdown.notification-dropdown.pull-right
+ .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
@@ -19,7 +18,7 @@
= notification_title(notification_setting.level)
= icon("caret-down")
- = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align
+ = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
= content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index a82fc95df84..b5c0a7fd6d4 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -18,7 +18,7 @@
%p
Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out
= succeed "." do
- %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank"} notification emails
+ %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank" } notification emails
.col-lg-8
- NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index|
- field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml
index d3258ee64cb..85ad74f9a39 100644
--- a/app/views/shared/notifications/_notification_dropdown.html.haml
+++ b/app/views/shared/notifications/_notification_dropdown.html.haml
@@ -1,5 +1,4 @@
-- left_align = local_assigns[:left_align]
-%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
+%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] }
- NotificationSetting.levels.each_key do |level|
- next if level == "custom"
- next if level == "global" && notification_setting.source.nil?
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 264391fe84f..4a27965754d 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -25,7 +25,7 @@
%span
= icon('star')
= number_with_delimiter(project.star_count)
- %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
= visibility_level_icon(project.visibility_level, fw: true)
.title
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index dcdba01aee9..ad5c0c2d8c8 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,6 +1,6 @@
- unless @snippet.content.empty?
- if markup?(@snippet.file_name)
- %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
+ %textarea.markdown-snippet-copy.blob-content{ data: { blob_id: @snippet.id } }
= @snippet.content
.file-content.wiki
- if gitlab_markdown?(@snippet.file_name)
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index d7506e07ff6..d084f5e9684 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -8,10 +8,6 @@
%span.creator
authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- - if @snippet.updated_at != @snippet.created_at
- %span
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
.snippet-actions
@@ -20,5 +16,9 @@
- else
= render "snippets/actions"
-%h2.snippet-title.prepend-top-0.append-bottom-0
- = markdown_field(@snippet, :title)
+.snippet-header
+ %h2.snippet-title.prepend-top-0.append-bottom-0
+ = markdown_field(@snippet, :title)
+
+ - if @snippet.updated_at != @snippet.created_at
+ = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago')
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index ea17bec8677..5d2d2317f22 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,17 +1,16 @@
+- link_project = local_assigns.fetch(:link_project, false)
+
%li.snippet-row
= image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
.title
= link_to reliable_snippet_path(snippet) do
= snippet.title
- - if snippet.private?
- %span.label.label-gray.hidden-xs
- = icon('lock')
- private
- %span.monospace.pull-right.hidden-xs
- = snippet.file_name
+ - if snippet.file_name
+ %span.snippet-filename.monospace.hidden-xs
+ = snippet.file_name
- %ul.controls.visible-xs
+ %ul.controls
%li
- note_count = snippet.notes.user.count
= link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
@@ -22,11 +21,17 @@
= visibility_level_label(snippet.visibility_level)
= visibility_level_icon(snippet.visibility_level, fw: false)
- %small.pull-right.cgray.hidden-xs
- - if snippet.project_id?
- = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project)
-
- .snippet-info.hidden-xs
+ .snippet-info
+ #{snippet.to_reference} &middot;
+ authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')}
+ by
= link_to user_snippets_path(snippet.author) do
= snippet.author_name
- authored #{time_ago_with_tooltip(snippet.created_at)}
+ - if link_project && snippet.project_id?
+ %span.hidden-xs
+ in
+ = link_to namespace_project_path(snippet.project.namespace, snippet.project) do
+ = snippet.project.name_with_namespace
+
+ .pull-right.snippet-updated-at
+ %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
new file mode 100644
index 00000000000..5074afb63a1
--- /dev/null
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -0,0 +1,9 @@
+- scopes = local_assigns.fetch(:scopes)
+- prefix = local_assigns.fetch(:prefix)
+- token = local_assigns.fetch(:token)
+
+- scopes.each do |scope|
+ %fieldset
+ = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
+ = label_tag "#{prefix}_scopes_#{scope}", scope
+ %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})"
diff --git a/app/views/shared/tokens/_scopes_list.html.haml b/app/views/shared/tokens/_scopes_list.html.haml
new file mode 100644
index 00000000000..f99e905e95c
--- /dev/null
+++ b/app/views/shared/tokens/_scopes_list.html.haml
@@ -0,0 +1,13 @@
+- token = local_assigns.fetch(:token)
+
+- return unless token.scopes.present?
+
+%tr
+ %td
+ Scopes
+ %td
+ %ul.scopes-list.append-bottom-0
+ - token.scopes.each do |scope|
+ %li
+ %span.scope-name= scope
+ = "(#{t(scope, scope: [:doorkeeper, :scopes])})"
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 5d659eb83a9..13586a5a12a 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -1,6 +1,3 @@
-- page_title "Webhooks"
-- context_title = @project ? 'project' : 'group'
-
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml
index 94d4dd4fa7d..92151176fce 100644
--- a/app/views/sherlock/file_samples/show.html.haml
+++ b/app/views/sherlock/file_samples/show.html.haml
@@ -41,7 +41,7 @@
%th= t('sherlock.percent')
%tbody
- @file_sample.line_samples.each_with_index do |sample, index|
- %tr{class: sample.majority_of?(@file_sample.duration) ? 'slow' : ''}
+ %tr{ class: sample.majority_of?(@file_sample.duration) ? 'slow' : '' }
%td= index + 1
%td= sample.events
%td
diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml
index 7073c0f4d90..5a447f791dc 100644
--- a/app/views/sherlock/queries/_general.html.haml
+++ b/app/views/sherlock/queries/_general.html.haml
@@ -26,7 +26,7 @@
.panel.panel-default
.panel-heading
.pull-right
- %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button}
+ %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button }
%i.fa.fa-clipboard
%pre.hidden
= @query.formatted_query
@@ -41,7 +41,7 @@
.panel.panel-default
.panel-heading
.pull-right
- %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button}
+ %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button }
%i.fa.fa-clipboard
%pre.hidden
= @query.explain
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
index fc2863dca8e..c45da6ee9a4 100644
--- a/app/views/sherlock/queries/show.html.haml
+++ b/app/views/sherlock/queries/show.html.haml
@@ -3,10 +3,10 @@
%ul.nav-links
%li.active
- %a(href="#tab-general" data-toggle="tab")
+ %a{ href: "#tab-general", data: { toggle: "tab" } }
= t('sherlock.general')
%li
- %a(href="#tab-backtrace" data-toggle="tab")
+ %a{ href: "#tab-backtrace", data: { toggle: "tab" } }
= t('sherlock.backtrace')
.row-content-block
diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml
index da969c02765..bc05659dfa8 100644
--- a/app/views/sherlock/transactions/index.html.haml
+++ b/app/views/sherlock/transactions/index.html.haml
@@ -28,7 +28,7 @@
%tr
%td= trans.type
%td
- %span{title: trans.path}
+ %span{ title: trans.path }
= truncate(trans.path, length: 70)
%td
= trans.duration.round(2)
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
index 8aa6b437d95..eab91e8fbe4 100644
--- a/app/views/sherlock/transactions/show.html.haml
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -3,15 +3,15 @@
%ul.nav-links
%li.active
- %a(href="#tab-general" data-toggle="tab")
+ %a{ href: "#tab-general", data: { toggle: "tab" } }
= t('sherlock.general')
%li
- %a(href="#tab-queries" data-toggle="tab")
+ %a{ href: "#tab-queries", data: { toggle: "tab" } }
= t('sherlock.queries')
%span.badge
#{@transaction.queries.length}
%li
- %a(href="#tab-file-samples" data-toggle="tab")
+ %a{ href: "#tab-file-samples", data: { toggle: "tab" } }
= t('sherlock.file_samples')
%span.badge
#{@transaction.file_samples.length}
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 1d0e549ed3d..95fc7198104 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
- - if current_user
- = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do
- New snippet
- - if can?(current_user, :admin_personal_snippet, @snippet)
- = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
- Delete
- if can?(current_user, :update_personal_snippet, @snippet)
- = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
+ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
Edit
+ - if can?(current_user, :admin_personal_snippet, @snippet)
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
+ Delete
+ - if current_user
+ = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
+ New snippet
- if current_user
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index 77b66ca74b6..ac3701233ad 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,8 +1,9 @@
- remote = local_assigns.fetch(:remote, false)
+- link_project = local_assigns.fetch(:link_project, false)
.snippets-list-holder
%ul.content-list
- = render partial: 'shared/snippets/snippet', collection: @snippets
+ = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
- if @snippets.empty?
%li
.nothing-here-block Nothing here.
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
new file mode 100644
index 00000000000..2dda5fed647
--- /dev/null
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -0,0 +1,31 @@
+- subject = local_assigns.fetch(:subject, current_user)
+- include_private = local_assigns.fetch(:include_private, false)
+
+.nav-links.snippet-scope-menu
+ %li{ class: ("active" unless params[:scope]) }
+ = link_to subject_snippets_path(subject) do
+ All
+ %span.badge
+ - if include_private
+ = subject.snippets.count
+ - else
+ = subject.snippets.public_and_internal.count
+
+ - if include_private
+ %li{ class: ("active" if params[:scope] == "are_private") }
+ = link_to subject_snippets_path(subject, scope: 'are_private') do
+ Private
+ %span.badge
+ = subject.snippets.are_private.count
+
+ %li{ class: ("active" if params[:scope] == "are_internal") }
+ = link_to subject_snippets_path(subject, scope: 'are_internal') do
+ Internal
+ %span.badge
+ = subject.snippets.are_internal.count
+
+ %li{ class: ("active" if params[:scope] == "are_public") }
+ = link_to subject_snippets_path(subject, scope: 'are_public') do
+ Public
+ %span.badge
+ = subject.snippets.are_public.count
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 27d7a6c5bb6..837a1a0cc8c 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -7,9 +7,9 @@
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
= link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
= link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm"
= render 'shared/snippets/blob'
-= render 'award_emoji/awards_block', awardable: @snippet, inline: true \ No newline at end of file
+= render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index 232ca26c1af..f878bece2fa 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -1,30 +1,21 @@
#js-authenticate-u2f
+%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
%script#js-authenticate-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
-%script#js-authenticate-u2f-setup{ type: "text/template" }
- %div
- %p Insert your security key (if you haven't already), and press the button below.
- %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Sign in via U2F device
-
%script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
%script#js-authenticate-u2f-error{ type: "text/template" }
%div
- %p <%= error_message %>
- %a.btn.btn-warning#js-u2f-try-again Try again?
+ %p <%= error_message %> (error code: <%= error_code %>)
+ %a.btn.btn-block.btn-warning#js-u2f-try-again Try again?
%script#js-authenticate-u2f-authenticated{ type: "text/template" }
%div
- %p We heard back from your U2F device. Click this button to authenticate with the GitLab server.
- = form_tag(new_user_session_path, method: :post) do |f|
+ %p We heard back from your U2F device. You have been authenticated.
+ = form_tag(new_user_session_path, method: :post, id: 'js-login-u2f-form') do |f|
- resource_params = params[resource_name].presence || params
= hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0)
= hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
- = submit_tag "Authenticate via U2F Device", class: "btn btn-success"
-
-:javascript
- var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f);
- u2fAuthenticate.start();
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index 8f7b42eb351..adc07bcba73 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -23,11 +23,11 @@
%script#js-register-u2f-error{ type: "text/template" }
%div
%p
- %span <%= error_message %>
+ %span <%= error_message %> (error code: <%= error_code %>)
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-register-u2f-registered{ type: "text/template" }
- %div.row.append-bottom-10
+ .row.append-bottom-10
.col-md-12
%p Your device was successfully set up! Give it a name and register it with the GitLab server.
= form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
index 09ff8a76d27..6228245d8d0 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -6,4 +6,4 @@
new Calendar(
#{@activity_dates.to_json},
'#{user_calendar_activities_path}'
- ); \ No newline at end of file
+ );
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 1e0752bd3c3..fb25eed4f37 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -18,11 +18,11 @@
- elsif current_user
- if @user.abuse_report
%button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }}
+ data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
= icon('exclamation-circle')
- else
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- if current_user
= link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
@@ -101,12 +101,12 @@
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
- .user-calendar{data: {href: user_calendar_path}}
+ .user-calendar{ data: { href: user_calendar_path } }
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
- .content_list{ data: {href: user_path} }
+ .content_list{ data: { href: user_path } }
= spinner
#groups.tab-pane
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
new file mode 100644
index 00000000000..2badd0680fb
--- /dev/null
+++ b/app/workers/authorized_projects_worker.rb
@@ -0,0 +1,14 @@
+class AuthorizedProjectsWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def self.bulk_perform_async(args_list)
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
+ end
+
+ def perform(user_id)
+ user = User.find_by(id: user_id)
+
+ user.refresh_authorized_projects if user
+ end
+end
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
new file mode 100644
index 00000000000..fa9e097e40a
--- /dev/null
+++ b/app/workers/build_queue_worker.rb
@@ -0,0 +1,10 @@
+class BuildQueueWorker
+ include Sidekiq::Worker
+ include BuildQueue
+
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id).try do |build|
+ Ci::UpdateBuildQueueService.new.execute(build)
+ end
+ end
+end
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index e0ad5268664..e17add7421f 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -4,15 +4,13 @@ class BuildSuccessWorker
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
- create_deployment(build)
+ create_deployment(build) if build.has_environment?
end
end
private
def create_deployment(build)
- return if build.environment.blank?
-
service = CreateDeploymentService.new(
build.project, build.user,
environment: build.environment,
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
new file mode 100644
index 00000000000..f870da4ecfd
--- /dev/null
+++ b/app/workers/delete_merged_branches_worker.rb
@@ -0,0 +1,20 @@
+class DeleteMergedBranchesWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(project_id, user_id)
+ begin
+ project = Project.find(project_id)
+ rescue ActiveRecord::RecordNotFound
+ return
+ end
+
+ user = User.find(user_id)
+
+ begin
+ DeleteMergedBranchesService.new(project, user).execute
+ rescue Gitlab::Access::AccessDeniedError
+ return
+ end
+ end
+end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index c3e62bb88c0..926162b8c53 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -2,10 +2,14 @@ class NewNoteWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
- def perform(note_id, note_params)
- note = Note.find(note_id)
-
- NotificationService.new.new_note(note)
- Notes::PostProcessService.new(note).execute
+ # Keep extra parameter to preserve backwards compatibility with
+ # old `NewNoteWorker` jobs (can remove later)
+ def perform(note_id, _params = {})
+ if note = Note.find_by(id: note_id)
+ NotificationService.new.new_note(note)
+ Notes::PostProcessService.new(note).execute
+ else
+ Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
+ end
end
end
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index 34f6ef161fb..070943f1ecc 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -12,11 +12,11 @@ class PipelineMetricsWorker
private
def update_metrics_for_active_pipeline(pipeline)
- metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
+ metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil, pipeline_id: pipeline.id)
end
def update_metrics_for_succeeded_pipeline(pipeline)
- metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at)
+ metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at, pipeline_id: pipeline.id)
end
def metrics(pipeline)
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 2aa6fff24da..cc0eb708cf9 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -4,7 +4,7 @@ class PipelineSuccessWorker
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
- MergeRequests::MergeWhenBuildSucceedsService
+ MergeRequests::MergeWhenPipelineSucceedsService
.new(pipeline.project, nil)
.trigger(pipeline)
end
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 071741fbacd..e9a5bd7f24e 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -10,9 +10,10 @@ class ProcessCommitWorker
# project_id - The ID of the project this commit belongs to.
# user_id - The ID of the user that pushed the commit.
- # commit_sha - The SHA1 of the commit to process.
+ # commit_hash - Hash containing commit details to use for constructing a
+ # Commit object without having to use the Git repository.
# default - The data was pushed to the default branch.
- def perform(project_id, user_id, commit_sha, default = false)
+ def perform(project_id, user_id, commit_hash, default = false)
project = Project.find_by(id: project_id)
return unless project
@@ -21,10 +22,7 @@ class ProcessCommitWorker
return unless user
- commit = find_commit(project, commit_sha)
-
- return unless commit
-
+ commit = build_commit(project, commit_hash)
author = commit.author || user
process_commit_message(project, commit, user, author, default)
@@ -59,9 +57,18 @@ class ProcessCommitWorker
update_all(first_mentioned_in_commit_at: commit.committed_date)
end
- private
+ def build_commit(project, hash)
+ date_suffix = '_date'
+
+ # When processing Sidekiq payloads various timestamps are stored as Strings.
+ # Commit in turn expects Time-like instances upon input, so we have to
+ # manually parse these values.
+ hash.each do |key, value|
+ if key.to_s.end_with?(date_suffix) && value.is_a?(String)
+ hash[key] = Time.parse(value)
+ end
+ end
- def find_commit(project, sha)
- project.commit(sha)
+ Commit.from_hash(hash, project)
end
end
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 4dfa745fb50..8ff9d07860f 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -1,54 +1,39 @@
# Worker for updating any project specific caches.
-#
-# This worker runs at most once every 15 minutes per project. This is to ensure
-# that multiple instances of jobs for this worker don't hammer the underlying
-# storage engine as much.
class ProjectCacheWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
LEASE_TIMEOUT = 15.minutes.to_i
- def self.lease_for(project_id)
- Gitlab::ExclusiveLease.
- new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT)
- end
-
- # Overwrite Sidekiq's implementation so we only schedule when actually needed.
- def self.perform_async(project_id)
- # If a lease for this project is still being held there's no point in
- # scheduling a new job.
- super unless lease_for(project_id).exists?
- end
+ # project_id - The ID of the project for which to flush the cache.
+ # files - An Array containing extra types of files to refresh such as
+ # `:readme` to flush the README and `:changelog` to flush the
+ # CHANGELOG.
+ # statistics - An Array containing columns from ProjectStatistics to
+ # refresh, if empty all columns will be refreshed
+ def perform(project_id, files = [], statistics = [])
+ project = Project.find_by(id: project_id)
- def perform(project_id)
- if try_obtain_lease_for(project_id)
- Rails.logger.
- info("Obtained ProjectCacheWorker lease for project #{project_id}")
- else
- Rails.logger.
- info("Could not obtain ProjectCacheWorker lease for project #{project_id}")
+ return unless project && project.repository.exists?
- return
- end
+ update_statistics(project, statistics.map(&:to_sym))
- update_caches(project_id)
+ project.repository.refresh_method_caches(files.map(&:to_sym))
end
- def update_caches(project_id)
- project = Project.find(project_id)
+ def update_statistics(project, statistics = [])
+ return unless try_obtain_lease_for(project.id, :update_statistics)
- return unless project.repository.exists?
+ Rails.logger.info("Updating statistics for project #{project.id}")
- project.update_repository_size
- project.update_commit_count
-
- if project.repository.root_ref
- project.repository.build_cache
- end
+ project.statistics.refresh!(only: statistics)
end
- def try_obtain_lease_for(project_id)
- self.class.lease_for(project_id).try_obtain
+ private
+
+ def try_obtain_lease_for(project_id, section)
+ Gitlab::ExclusiveLease.
+ new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT).
+ try_obtain
end
end
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
new file mode 100644
index 00000000000..18b8daf4e1e
--- /dev/null
+++ b/app/workers/reactive_caching_worker.rb
@@ -0,0 +1,15 @@
+class ReactiveCachingWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(class_name, id, *args)
+ klass = begin
+ Kernel.const_get(class_name)
+ rescue NameError
+ nil
+ end
+ return unless klass
+
+ klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args)
+ end
+end
diff --git a/app/workers/use_key_worker.rb b/app/workers/use_key_worker.rb
new file mode 100644
index 00000000000..c9d382cc5d6
--- /dev/null
+++ b/app/workers/use_key_worker.rb
@@ -0,0 +1,13 @@
+class UseKeyWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(key_id)
+ key = Key.find(key_id)
+ key.touch(:last_used_at)
+ rescue ActiveRecord::RecordNotFound
+ Rails.logger.error("UseKeyWorker: couldn't find key with ID=#{key_id}, skipping job")
+
+ false
+ end
+end