Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Bennett <lukeeeebennettplus@gmail.com>2018-05-28 14:27:06 +0300
committerLuke Bennett <lukeeeebennettplus@gmail.com>2018-05-28 14:27:06 +0300
commita9583bc9562512bd046a7f2c32fd267f32a5dac2 (patch)
treeaf91d0688d49850746de400afdacb5389c0cfb16 /app/assets/javascripts
parenta3e472e0a7a6b6ec5654edd20b947ba660ed2dc3 (diff)
parent265b1fafe64ae9fe8a3e92d83c1678b47533ba86 (diff)
Merge remote-tracking branch 'origin/master' into 39549-label-list-page-redesign-with-draggable-labels
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/api.js30
-rw-r--r--app/assets/javascripts/awards_handler.js8
-rw-r--r--app/assets/javascripts/badges/components/badge.vue6
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue10
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js4
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js27
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js4
-rw-r--r--app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js6
-rw-r--r--app/assets/javascripts/blob/sketch/index.js2
-rw-r--r--app/assets/javascripts/blob/viewer/index.js2
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue6
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js12
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js4
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js5
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js26
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue2
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue4
-rw-r--r--app/assets/javascripts/clusters/components/gcp_signup_offer.js27
-rw-r--r--app/assets/javascripts/commons/bootstrap.js10
-rw-r--r--app/assets/javascripts/compare.js86
-rw-r--r--app/assets/javascripts/compare_autocomplete.js51
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js43
-rw-r--r--app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_service.js30
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue71
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue212
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue322
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue100
-rw-r--r--app/assets/javascripts/deploy_keys/index.js39
-rw-r--r--app/assets/javascripts/deploy_keys/service/index.js28
-rw-r--r--app/assets/javascripts/deploy_keys/store/index.js4
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js4
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js2
-rw-r--r--app/assets/javascripts/due_date_select.js9
-rw-r--r--app/assets/javascripts/emoji/index.js6
-rw-r--r--app/assets/javascripts/emoji/support/unicode_support_map.js40
-rw-r--r--app/assets/javascripts/environments/components/container.vue1
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue20
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue6
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue17
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue5
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue4
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue20
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue3
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js26
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js17
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js9
-rw-r--r--app/assets/javascripts/gl_dropdown.js4
-rw-r--r--app/assets/javascripts/gl_field_error.js10
-rw-r--r--app/assets/javascripts/gpg_badges.js6
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue2
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue106
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue75
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue18
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue36
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue171
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue164
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue121
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue61
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue30
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue59
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/success_message.vue33
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue45
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue80
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue243
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue113
-rw-r--r--app/assets/javascripts/ide/components/ide.vue170
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue84
-rw-r--r--app/assets/javascripts/ide/components/ide_external_links.vue43
-rw-r--r--app/assets/javascripts/ide/components/ide_file_buttons.vue10
-rw-r--r--app/assets/javascripts/ide/components/ide_project_branches_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_project_tree.vue65
-rw-r--r--app/assets/javascripts/ide/components/ide_repo_tree.vue41
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue62
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue143
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue134
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue42
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue76
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue4
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue97
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue13
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue170
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue70
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue114
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue50
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue17
-rw-r--r--app/assets/javascripts/ide/constants.js19
-rw-r--r--app/assets/javascripts/ide/ide_router.js25
-rw-r--r--app/assets/javascripts/ide/index.js33
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js43
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js4
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js9
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js25
-rw-r--r--app/assets/javascripts/ide/lib/editor.js45
-rw-r--r--app/assets/javascripts/ide/lib/keymap.json11
-rw-r--r--app/assets/javascripts/ide/services/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions.js62
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js53
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js82
-rw-r--r--app/assets/javascripts/ide/stores/getters.js62
-rw-r--r--app/assets/javascripts/ide/stores/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js88
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js14
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js49
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/index.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js53
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/state.js6
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js13
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js57
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js14
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js104
-rw-r--r--app/assets/javascripts/ide/stores/state.js8
-rw-r--r--app/assets/javascripts/ide/stores/utils.js18
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js12
-rw-r--r--app/assets/javascripts/issuable_context.js4
-rw-r--r--app/assets/javascripts/issuable_form.js2
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue2
-rw-r--r--app/assets/javascripts/job.js11
-rw-r--r--app/assets/javascripts/jobs/components/header.vue150
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue185
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js5
-rw-r--r--app/assets/javascripts/label_manager.js2
-rw-r--r--app/assets/javascripts/labels_select.js7
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js98
-rw-r--r--app/assets/javascripts/lib/utils/keycodes.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js6
-rw-r--r--app/assets/javascripts/main.js69
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js2
-rw-r--r--app/assets/javascripts/milestone_select.js83
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js4
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue22
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue22
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue6
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js20
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js2
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js1
-rw-r--r--app/assets/javascripts/notes.js10
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue11
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue10
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue35
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/stores/actions.js3
-rw-r--r--app/assets/javascripts/notes/stores/getters.js3
-rw-r--r--app/assets/javascripts/notifications_form.js2
-rw-r--r--app/assets/javascripts/pages/admin/admin.js2
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue5
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js4
-rw-r--r--app/assets/javascripts/pages/ide/index.js9
-rw-r--r--app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js3
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/login/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/clusters/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/compare/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/compare/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js54
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js60
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js22
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue77
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js28
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js205
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js4
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue5
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue2
-rw-r--r--app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue20
-rw-r--r--app/assets/javascripts/performance_bar/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/async_button.vue95
-rw-r--r--app/assets/javascripts/pipelines/components/blank_state.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue60
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue53
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue135
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue56
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue107
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue261
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.vue2
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js4
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js31
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue6
-rw-r--r--app/assets/javascripts/project_fork.js2
-rw-r--r--app/assets/javascripts/project_label_subscription.js6
-rw-r--r--app/assets/javascripts/project_visibility.js2
-rw-r--r--app/assets/javascripts/projects/project_new.js4
-rw-r--r--app/assets/javascripts/projects_dropdown/components/app.vue5
-rw-r--r--app/assets/javascripts/projects_dropdown/components/search.vue2
-rw-r--r--app/assets/javascripts/projects_dropdown/service/projects_service.js14
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js2
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue9
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue16
-rw-r--r--app/assets/javascripts/registry/stores/actions.js3
-rw-r--r--app/assets/javascripts/registry/stores/getters.js3
-rw-r--r--app/assets/javascripts/right_sidebar.js14
-rw-r--r--app/assets/javascripts/shortcuts.js1
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js5
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue21
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue12
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue22
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue24
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue23
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue33
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js10
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue (renamed from app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js)35
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js15
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue12
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js3
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js3
-rw-r--r--app/assets/javascripts/user_callout.js9
-rw-r--r--app/assets/javascripts/users_select.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue104
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js)6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue (renamed from app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js)79
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue (renamed from app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js)168
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/callout.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue88
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue75
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue76
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue215
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/deprecated_modal.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue54
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue101
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue136
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue123
-rw-r--r--app/assets/javascripts/vue_shared/components/identicon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue39
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue4
-rw-r--r--app/assets/javascripts/vue_shared/directives/popover.js2
-rw-r--r--app/assets/javascripts/vue_shared/directives/tooltip.js4
-rw-r--r--app/assets/javascripts/vue_shared/models/label.js2
290 files changed, 5981 insertions, 3397 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 8ad3d18b302..ce1069276ab 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -21,8 +21,11 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
+ commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
+ pipelinesPath: '/api/:version/projects/:id/pipelines',
+ pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -164,6 +167,19 @@ const Api = {
});
},
+ commitPipelines(projectId, sha) {
+ const encodedProjectId = projectId
+ .split('/')
+ .map(fragment => encodeURIComponent(fragment))
+ .join('/');
+
+ const url = Api.buildUrl(Api.commitPipelinesPath)
+ .replace(':project_id', encodedProjectId)
+ .replace(':sha', encodeURIComponent(sha));
+
+ return axios.get(url);
+ },
+
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
@@ -222,6 +238,20 @@ const Api = {
});
},
+ pipelines(projectPath, params = {}) {
+ const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath));
+
+ return axios.get(url, { params });
+ },
+
+ pipelineJobs(projectPath, pipelineId, params = {}) {
+ const url = Api.buildUrl(this.pipelineJobsPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':pipeline_id', pipelineId);
+
+ return axios.get(url, { params });
+ },
+
buildUrl(url) {
let urlRoot = '';
if (gon.relative_url_root != null) {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 976d32abe9b..eb0f06efab4 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -345,7 +345,7 @@ class AwardsHandler {
counter.text(counterNumber - 1);
this.removeYouFromUserList($emojiButton);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
- $emojiButton.tooltip('destroy');
+ $emojiButton.tooltip('dispose');
counter.text('0');
this.removeYouFromUserList($emojiButton);
if ($emojiButton.parents('.note').length) {
@@ -358,7 +358,7 @@ class AwardsHandler {
}
removeEmoji($emojiButton) {
- $emojiButton.tooltip('destroy');
+ $emojiButton.tooltip('dispose');
$emojiButton.remove();
const $votesBlock = this.getVotesBlock();
if ($votesBlock.find('.js-emoji-btn').length === 0) {
@@ -392,7 +392,7 @@ class AwardsHandler {
.removeAttr('data-title')
.removeAttr('data-original-title')
.attr('title', this.toSentence(authors))
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
}
addYouToUserList(votesBlock, emoji) {
@@ -405,7 +405,7 @@ class AwardsHandler {
users.unshift('You');
return awardBlock
.attr('title', this.toSentence(users))
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
}
createAwardButtonForVotesBlock(votesBlock, emojiName) {
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 6e6cb31e3ac..d0f60e1d4cb 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -89,7 +89,7 @@ export default {
v-show="hasError"
class="btn-group"
>
- <div class="btn btn-default btn-xs disabled">
+ <div class="btn btn-default btn-sm disabled">
<icon
class="prepend-left-8 append-right-8"
name="doc_image"
@@ -98,7 +98,7 @@ export default {
/>
</div>
<div
- class="btn btn-default btn-xs disabled"
+ class="btn btn-default btn-sm disabled"
>
<span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span>
</div>
@@ -106,7 +106,7 @@ export default {
<button
v-show="hasError"
- class="btn btn-transparent btn-xs text-primary"
+ class="btn btn-transparent btn-sm text-primary"
type="button"
v-tooltip
:title="s__('Badges|Reload badge image')"
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index ca7197e1e0f..268968b63b3 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -23,8 +23,8 @@ export default {
</script>
<template>
- <div class="panel panel-default">
- <div class="panel-heading">
+ <div class="card">
+ <div class="card-header">
{{ s__('Badges|Your badges') }}
<span
v-show="!isLoading"
@@ -33,19 +33,19 @@ export default {
</div>
<loading-icon
v-show="isLoading"
- class="panel-body"
+ class="card-body"
size="2"
/>
<div
v-if="hasNoBadges"
- class="panel-body"
+ class="card-body"
>
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
<span v-else>{{ s__('Badges|This project has no badges') }}</span>
</div>
<div
v-else
- class="panel-body"
+ class="card-body"
>
<badge-list-row
v-for="badge in badges"
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index e2a73a1797c..75834ba351d 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -8,10 +8,10 @@ function showTooltip(target, title) {
if (!$target.data('hideTooltip')) {
$target
.attr('title', title)
- .tooltip('fixTitle')
+ .tooltip('_fixTitle')
.tooltip('show')
.attr('title', originalTitle)
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
}
}
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index 7e98e04303a..56293d5f96f 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -7,27 +7,24 @@ export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
- const {
- name,
- unicodeVersion,
- fallbackSrc,
- fallbackSpriteClass,
- } = this.dataset;
+ const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
- const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
- this.childNodes,
- childNode => childNode.nodeType === 3,
- );
+ const isEmojiUnicode =
+ this.childNodes &&
+ Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
- if (
- emojiUnicode &&
- isEmojiUnicode &&
- !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
- ) {
+ if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) {
+ if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
+ const emojiSpriteLinkTag = document.createElement('link');
+ emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
+ emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
+ document.head.appendChild(emojiSpriteLinkTag);
+ gon.emoji_sprites_css_added = true;
+ }
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 3ec932bdb73..b6e2781773c 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -69,7 +69,7 @@ $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-q
$this.tooltip({
container: 'body',
html: 'true',
- placement: 'auto top',
+ placement: 'top',
title,
trigger: 'manual',
});
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index ffff4ddb71a..a8b6dbf0948 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -42,9 +42,9 @@ $.fn.requiresInput = function requiresInput() {
function hideOrShowHelpBlock(form) {
const selected = $('.js-select-namespace option:selected');
if (selected.length && selected.data('optionsParent') === 'groups') {
- form.find('.help-block').hide();
+ form.find('.form-text.text-muted').hide();
} else if (selected.length) {
- form.find('.help-block').show();
+ form.find('.form-text.text-muted').show();
}
}
diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
index c17877a276d..766039404ce 100644
--- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
@@ -2,9 +2,9 @@ import sqljs from 'sql.js';
import { template as _template } from 'underscore';
const PREVIEW_TEMPLATE = _template(`
- <div class="panel panel-default">
- <div class="panel-heading"><%- name %></div>
- <div class="panel-body">
+ <div class="card">
+ <div class="card-header"><%- name %></div>
+ <div class="card-body">
<img class="img-thumbnail" src="data:image/png;base64,<%- image %>"/>
</div>
</div>
diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js
index 0799991aa40..13318c58006 100644
--- a/app/assets/javascripts/blob/sketch/index.js
+++ b/app/assets/javascripts/blob/sketch/index.js
@@ -44,7 +44,7 @@ export default class SketchLoader {
previewLink.href = previewUrl;
previewLink.target = '_blank';
previewImage.src = previewUrl;
- previewImage.className = 'img-responsive';
+ previewImage.className = 'img-fluid';
previewLink.appendChild(previewImage);
this.container.appendChild(previewLink);
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 137e1f5a099..f61c0be9230 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -116,7 +116,7 @@ export default class BlobViewer {
this.copySourceBtn.classList.add('disabled');
}
- $(this.copySourceBtn).tooltip('fixTitle');
+ $(this.copySourceBtn).tooltip('_fixTitle');
}
switchToViewer(name) {
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 84885ca9306..33e3369b971 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -77,7 +77,7 @@ export default {
<template>
<li
- class="card"
+ class="board-card"
:class="{
'user-can-drag': !disabled && issue.id,
'is-disabled': disabled || !issue.id,
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 8d84c1735b8..e8dfd95f7ae 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -92,7 +92,7 @@ export default {
<template>
<div class="board-new-issue-form">
- <div class="card">
+ <div class="board-card">
<form @submit="submit($event)">
<div
class="flash-container"
@@ -122,7 +122,7 @@ export default {
/>
<div class="clearfix prepend-top-10">
<button
- class="btn btn-success pull-left"
+ class="btn btn-success float-left"
type="submit"
:disabled="disabled"
ref="submit-button"
@@ -130,7 +130,7 @@ export default {
Submit issue
</button>
<button
- class="btn btn-default pull-right"
+ class="btn btn-default float-right"
type="button"
@click="cancel"
>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index 84fe9b1288a..dcc07810d01 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -135,8 +135,8 @@ gl.issueBoards.IssueCardInner = Vue.extend({
},
template: `
<div>
- <div class="card-header">
- <h4 class="card-title">
+ <div class="board-card-header">
+ <h4 class="board-card-title">
<i
class="fa fa-eye-slash confidential-icon"
v-if="issue.confidential"
@@ -147,13 +147,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
:href="issue.path"
:title="issue.title">{{ issue.title }}</a>
<span
- class="card-number"
+ class="board-card-number"
v-if="issueId"
>
{{ issue.referencePath }}
</span>
</h4>
- <div class="card-assignee">
+ <div class="board-card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
:key="assignee.id"
@@ -175,11 +175,11 @@ gl.issueBoards.IssueCardInner = Vue.extend({
</div>
</div>
<div
- class="card-footer"
+ class="board-card-footer"
v-if="showLabelFooter"
>
<button
- class="label color-label has-tooltip"
+ class="badge color-label has-tooltip"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index 9e37f95cdd6..eb8a66975ee 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
template: `
<section class="empty-state">
<div class="row">
- <div class="col-xs-12 col-sm-6 col-sm-push-6">
+ <div class="col-xs-12 col-sm-6 order-sm-last">
<aside class="svg-content"><img :src="emptyStateSvg"/></aside>
</div>
- <div class="col-xs-12 col-sm-6 col-sm-pull-6">
+ <div class="col-xs-12 col-sm-6 order-sm-first">
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index 9735e0ddacc..11bb3e98334 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -58,7 +58,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
template: `
<footer
class="form-actions add-issues-footer">
- <div class="pull-left">
+ <div class="float-left">
<button
class="btn btn-success"
type="button"
@@ -72,7 +72,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
<lists-dropdown></lists-dropdown>
</div>
<button
- class="btn btn-default pull-right"
+ class="btn btn-default float-right"
type="button"
@click="toggleModal(false)">
Cancel
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
index 6b04a6c7a6c..6c662432037 100644
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -133,9 +133,9 @@ gl.issueBoards.ModalList = Vue.extend({
<div
v-for="issue in group"
v-if="showIssue(issue)"
- class="card-parent">
+ class="board-card-parent">
<div
- class="card"
+ class="board-card"
:class="{ 'is-active': issue.selected }"
@click="toggleIssue($event, issue)">
<issue-card-inner
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
index b6465a88e5e..9d331de8e22 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -24,7 +24,7 @@ gl.issueBoards.ModalTabs = Vue.extend({
role="button"
@click.prevent="changeTab('all')">
Open issues
- <span class="badge">
+ <span class="badge badge-pill">
{{ issuesCount }}
</span>
</a>
@@ -35,7 +35,7 @@ gl.issueBoards.ModalTabs = Vue.extend({
role="button"
@click.prevent="changeTab('selected')">
Selected issues
- <span class="badge">
+ <span class="badge badge-pill">
{{ selectedCount }}
</span>
</a>
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index a6f8681cfac..29ab13b8e0b 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -214,7 +214,7 @@ export default () => {
if (this.disabled) {
$tooltip.tooltip();
} else {
- $tooltip.tooltip('destroy');
+ $tooltip.tooltip('dispose');
}
});
},
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index e210d69895e..7144f4190e7 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -113,6 +113,8 @@ class List {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
+ issue.path = data.real_path;
+ issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index e177a3bfdc7..47efb3a8cee 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -141,6 +141,11 @@ export default class VariableList {
$rowClone.find(entry.selector).val(entry.default);
});
+ // Close any dropdowns
+ $rowClone.find('.dropdown-menu.show').each((index, $dropdown) => {
+ $dropdown.classList.remove('show');
+ });
+
this.initRow($rowClone);
$row.after($rowClone);
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
index 2e3ad244375..1e5c733d151 100644
--- a/app/assets/javascripts/clusters/clusters_index.js
+++ b/app/assets/javascripts/clusters/clusters_index.js
@@ -1,20 +1,24 @@
-import Flash from '../flash';
-import { s__ } from '../locale';
-import setupToggleButtons from '../toggle_buttons';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import setupToggleButtons from '~/toggle_buttons';
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
+
+ gcpSignupOffer();
+
// The empty state won't have a clusterList
if (clusterList) {
- setupToggleButtons(
- document.querySelector('.js-clusters-list'),
- (value, toggle) =>
- ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } })
- .catch((err) => {
- Flash(s__('ClusterIntegration|Something went wrong on our end.'));
- throw err;
- }),
+ setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
+ ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
+ err => {
+ createFlash(__('Something went wrong on our end.'));
+ throw err;
+ },
+ ),
);
}
};
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index c2a35341eb2..fae580c091b 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -179,7 +179,7 @@
role="row"
>
<div
- class="alert alert-danger alert-block append-bottom-0 table-section section-100"
+ class="alert alert-danger alert-block append-bottom-0"
role="gridcell"
>
<div>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 9c12b89240c..bb5fcea648d 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -191,11 +191,11 @@ export default {
:value="ingressExternalIp"
readonly
/>
- <span class="input-group-btn">
+ <span class="input-group-append">
<clipboard-button
:text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
- class="js-clipboard-btn"
+ class="input-group-text js-clipboard-btn"
/>
</span>
</div>
diff --git a/app/assets/javascripts/clusters/components/gcp_signup_offer.js b/app/assets/javascripts/clusters/components/gcp_signup_offer.js
new file mode 100644
index 00000000000..8bc20a1c09f
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/gcp_signup_offer.js
@@ -0,0 +1,27 @@
+import $ from 'jquery';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+
+export default function gcpSignupOffer() {
+ const alertEl = document.querySelector('.gcp-signup-offer');
+ if (!alertEl) {
+ return;
+ }
+
+ const closeButtonEl = alertEl.getElementsByClassName('close')[0];
+ const { dismissEndpoint, featureId } = closeButtonEl.dataset;
+
+ closeButtonEl.addEventListener('click', () => {
+ axios
+ .post(dismissEndpoint, {
+ feature_name: featureId,
+ })
+ .then(() => {
+ $(alertEl).alert('close');
+ })
+ .catch(() => {
+ Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
+ });
+ });
+}
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
index db96da4ccba..50e2949ab55 100644
--- a/app/assets/javascripts/commons/bootstrap.js
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -1,15 +1,7 @@
import $ from 'jquery';
// bootstrap jQuery plugins
-import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
-import 'bootstrap-sass/assets/javascripts/bootstrap/alert';
-import 'bootstrap-sass/assets/javascripts/bootstrap/button';
-import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown';
-import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
-import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
-import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
-import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
-import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
+import 'bootstrap';
// custom jQuery functions
$.fn.extend({
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
deleted file mode 100644
index 303a5bf4a53..00000000000
--- a/app/assets/javascripts/compare.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
-
-import $ from 'jquery';
-import { localTimeAgo } from './lib/utils/datetime_utility';
-import axios from './lib/utils/axios_utils';
-
-export default class Compare {
- constructor(opts) {
- this.opts = opts;
- this.source_loading = $(".js-source-loading");
- this.target_loading = $(".js-target-loading");
- $('.js-compare-dropdown').each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return $dropdown.glDropdown({
- selectable: true,
- fieldName: $dropdown.data('fieldName'),
- filterable: true,
- id: function(obj, $el) {
- return $el.data('id');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- },
- clicked: function(e, el) {
- if ($dropdown.is('.js-target-branch')) {
- return _this.getTargetHtml();
- } else if ($dropdown.is('.js-source-branch')) {
- return _this.getSourceHtml();
- } else if ($dropdown.is('.js-target-project')) {
- return _this.getTargetProject();
- }
- }
- });
- };
- })(this));
- this.initialState();
- }
-
- initialState() {
- this.getSourceHtml();
- this.getTargetHtml();
- }
-
- getTargetProject() {
- $('.mr_target_commit').empty();
-
- return axios.get(this.opts.targetProjectUrl, {
- params: {
- target_project_id: $("input[name='merge_request[target_project_id]']").val(),
- },
- }).then(({ data }) => {
- $('.js-target-branch-dropdown .dropdown-content').html(data);
- });
- }
-
- getSourceHtml() {
- return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
- ref: $("input[name='merge_request[source_branch]']").val()
- });
- }
-
- getTargetHtml() {
- return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
- target_project_id: $("input[name='merge_request[target_project_id]']").val(),
- ref: $("input[name='merge_request[target_branch]']").val()
- });
- }
-
- static sendAjax(url, loading, target, params) {
- const $target = $(target);
-
- loading.show();
- $target.empty();
-
- return axios.get(url, {
- params,
- }).then(({ data }) => {
- loading.hide();
- $target.html(data);
- const className = '.' + $target[0].className.replace(' ', '.');
- localTimeAgo($('.js-timeago', className));
- });
- }
-}
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 260c91cac24..ffe15f02f2e 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -4,8 +4,9 @@ import $ from 'jquery';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+import { capitalizeFirstCharacter } from './lib/utils/text_utility';
-export default function initCompareAutocomplete() {
+export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
@@ -15,14 +16,27 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({
data: function(term, callback) {
- axios.get($dropdown.data('refsUrl'), {
- params: {
- ref: $dropdown.data('ref'),
- search: term,
- },
- }).then(({ data }) => {
- callback(data);
- }).catch(() => flash(__('Error fetching refs')));
+ const params = {
+ ref: $dropdown.data('ref'),
+ search: term,
+ };
+
+ if (limitTo) {
+ params.find = limitTo;
+ }
+
+ axios
+ .get($dropdown.data('refsUrl'), {
+ params,
+ })
+ .then(({ data }) => {
+ if (limitTo) {
+ callback(data[capitalizeFirstCharacter(limitTo)] || []);
+ } else {
+ callback(data);
+ }
+ })
+ .catch(() => flash(__('Error fetching refs')));
},
selectable: true,
filterable: true,
@@ -32,9 +46,15 @@ export default function initCompareAutocomplete() {
renderRow: function(ref) {
var link;
if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
+ return $('<li />')
+ .addClass('dropdown-header')
+ .text(ref.header);
} else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ link = $('<a />')
+ .attr('href', '#')
+ .addClass(ref === selected ? 'is-active' : '')
+ .text(ref)
+ .attr('data-ref', ref);
return $('<li />').append(link);
}
},
@@ -43,9 +63,10 @@ export default function initCompareAutocomplete() {
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
- }
+ },
+ clicked: () => clickHandler($dropdown),
});
- $filterInput.on('keyup', (e) => {
+ $filterInput.on('keyup', e => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
@@ -54,10 +75,10 @@ export default function initCompareAutocomplete() {
$dropdownContainer.removeClass('open');
});
- $dropdownContainer.on('click', '.dropdown-content a', (e) => {
+ $dropdownContainer.on('click', '.dropdown-content a', e => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
- $dropdown.tooltip('fixTitle');
+ $dropdown.tooltip('_fixTitle');
}
});
});
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index fb1fc9cd32e..09d490106df 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -61,8 +61,8 @@ export default class CreateMergeRequestDropdown {
}
available() {
- this.availableButton.classList.remove('hide');
- this.unavailableButton.classList.add('hide');
+ this.availableButton.classList.remove('hidden');
+ this.unavailableButton.classList.add('hidden');
}
bindEvents() {
@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) {
this.available();
this.enable();
+ this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
- } else if (data.has_related_branch) {
+ } else {
this.hide();
}
})
.catch(() => {
this.unavailable();
this.disable();
- Flash('Failed to check if a new branch can be created.');
+ Flash(__('Failed to check related branches.'));
});
}
@@ -231,7 +232,7 @@ export default class CreateMergeRequestDropdown {
}
hide() {
- this.wrapperEl.classList.add('hide');
+ this.wrapperEl.classList.add('hidden');
}
init() {
@@ -405,8 +406,13 @@ export default class CreateMergeRequestDropdown {
}
unavailable() {
- this.availableButton.classList.add('hide');
- this.unavailableButton.classList.remove('hide');
+ this.availableButton.classList.add('hidden');
+ this.unavailableButton.classList.remove('hidden');
+ }
+
+ updateBranchName(suggestedBranchName) {
+ this.branchInput.value = suggestedBranchName;
+ this.updateCreatePaths('branch', suggestedBranchName);
}
updateInputState(target, ref, result) {
@@ -414,8 +420,6 @@ export default class CreateMergeRequestDropdown {
// ref - string - what a user typed.
// result - string - what has been found on backend.
- const pathReplacement = `$1${ref}`;
-
// If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists.
if (ref === result) {
@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refInput.dataset.value = ref;
this.showAvailableMessage('ref');
- this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
- pathReplacement);
- this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
- pathReplacement);
+ this.updateCreatePaths(target, ref);
}
} else if (target === 'branch') {
this.branchIsValid = true;
this.showAvailableMessage('branch');
- this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
- pathReplacement);
- this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
- pathReplacement);
+ this.updateCreatePaths(target, ref);
} else {
this.refIsValid = false;
this.refInput.dataset.value = ref;
@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction();
}
}
+
+ // target - 'branch' or 'ref'
+ // ref - string - the new value to use as branch or ref
+ updateCreatePaths(target, ref) {
+ const pathReplacement = `$1${ref}`;
+
+ this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
+ pathReplacement);
+ this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
+ pathReplacement);
+ }
}
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
index 32ae0cc1476..5be17081b58 100644
--- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
@@ -16,7 +16,7 @@
<template>
<span
v-if="count === 50"
- class="events-info pull-right"
+ class="events-info float-right"
>
<i
class="fa fa-warning"
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 87f8854f940..1c43fc3cdc7 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -82,7 +82,6 @@ export default () => {
this.service
.fetchCycleAnalyticsData(fetchOptions)
- .then(resp => resp.json())
.then((response) => {
this.store.setCycleAnalyticsData(response);
this.selectDefaultStage();
@@ -116,7 +115,6 @@ export default () => {
stage,
startDate: this.startDate,
})
- .then(resp => resp.json())
.then((response) => {
this.isEmptyStage = !response.events.length;
this.store.setStageEvents(response.events, stage);
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
index f496c38208d..4cf416c50e5 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
@@ -1,16 +1,20 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class CycleAnalyticsService {
constructor(options) {
- this.requestPath = options.requestPath;
- this.cycleAnalytics = Vue.resource(this.requestPath);
+ this.axios = axios.create({
+ baseURL: options.requestPath,
+ });
}
fetchCycleAnalyticsData(options = { startDate: 30 }) {
- return this.cycleAnalytics.get({ cycle_analytics: { start_date: options.startDate } });
+ return this.axios
+ .get('', {
+ params: {
+ 'cycle_analytics[start_date]': options.startDate,
+ },
+ })
+ .then(x => x.data);
}
fetchStageData(options) {
@@ -19,12 +23,12 @@ export default class CycleAnalyticsService {
startDate,
} = options;
- return Vue.http.get(`${this.requestPath}/events/${stage.name}.json`, {
- params: {
- cycle_analytics: {
- start_date: startDate,
+ return this.axios
+ .get(`events/${stage.name}.json`, {
+ params: {
+ 'cycle_analytics[start_date]': startDate,
},
- },
- });
+ })
+ .then(x => x.data);
}
}
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index b839b9f286f..67dda0e29cb 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -1,55 +1,50 @@
<script>
- import eventHub from '../eventhub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+import eventHub from '../eventhub';
- export default {
- components: {
- loadingIcon,
+export default {
+ components: {
+ loadingIcon,
+ },
+ props: {
+ deployKey: {
+ type: Object,
+ required: true,
},
- props: {
- deployKey: {
- type: Object,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- btnCssClass: {
- type: String,
- required: false,
- default: 'btn-default',
- },
+ type: {
+ type: String,
+ required: true,
},
- data() {
- return {
- isLoading: false,
- };
+ btnCssClass: {
+ type: String,
+ required: false,
+ default: 'btn-default',
},
- computed: {
- text() {
- return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
- },
- },
- methods: {
- doAction() {
- this.isLoading = true;
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ doAction() {
+ this.isLoading = true;
- eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
- this.isLoading = false;
- });
- },
+ eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
+ this.isLoading = false;
+ });
},
- };
+ },
+};
</script>
<template>
<button
- class="btn btn-sm prepend-left-10"
+ class="btn"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
@click="doAction">
- {{ text }}
+ <slot></slot>
<loading-icon
v-if="isLoading"
:inline="true"
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 5a782237b7d..c41fe55db63 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,80 +1,115 @@
<script>
- import Flash from '../../flash';
- import eventHub from '../eventhub';
- import DeployKeysService from '../service';
- import DeployKeysStore from '../store';
- import keysPanel from './keys_panel.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import { s__ } from '~/locale';
+import Flash from '~/flash';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+import eventHub from '../eventhub';
+import DeployKeysService from '../service';
+import DeployKeysStore from '../store';
+import KeysPanel from './keys_panel.vue';
- export default {
- components: {
- keysPanel,
- loadingIcon,
+export default {
+ components: {
+ KeysPanel,
+ LoadingIcon,
+ NavigationTabs,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
- props: {
- endpoint: {
- type: String,
- required: true,
- },
+ projectId: {
+ type: String,
+ required: true,
},
- data() {
- return {
- isLoading: false,
- store: new DeployKeysStore(),
- };
+ },
+ data() {
+ return {
+ currentTab: 'enabled_keys',
+ isLoading: false,
+ store: new DeployKeysStore(),
+ };
+ },
+ scopes: {
+ enabled_keys: s__('DeployKeys|Enabled deploy keys'),
+ available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
+ public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
+ },
+ computed: {
+ tabs() {
+ return Object.keys(this.$options.scopes).map(scope => {
+ const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null;
+
+ return {
+ name: this.$options.scopes[scope],
+ scope,
+ isActive: scope === this.currentTab,
+ count,
+ };
+ });
+ },
+ hasKeys() {
+ return Object.keys(this.keys).length;
},
- computed: {
- hasKeys() {
- return Object.keys(this.keys).length;
- },
- keys() {
- return this.store.keys;
- },
+ keys() {
+ return this.store.keys;
},
- created() {
- this.service = new DeployKeysService(this.endpoint);
+ },
+ created() {
+ this.service = new DeployKeysService(this.endpoint);
- eventHub.$on('enable.key', this.enableKey);
- eventHub.$on('remove.key', this.disableKey);
- eventHub.$on('disable.key', this.disableKey);
+ eventHub.$on('enable.key', this.enableKey);
+ eventHub.$on('remove.key', this.disableKey);
+ eventHub.$on('disable.key', this.disableKey);
+ },
+ mounted() {
+ this.fetchKeys();
+ },
+ beforeDestroy() {
+ eventHub.$off('enable.key', this.enableKey);
+ eventHub.$off('remove.key', this.disableKey);
+ eventHub.$off('disable.key', this.disableKey);
+ },
+ methods: {
+ onChangeTab(tab) {
+ this.currentTab = tab;
},
- mounted() {
- this.fetchKeys();
+ fetchKeys() {
+ this.isLoading = true;
+
+ return this.service
+ .getKeys()
+ .then(data => {
+ this.isLoading = false;
+ this.store.keys = data;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.store.keys = {};
+ return new Flash(s__('DeployKeys|Error getting deploy keys'));
+ });
},
- beforeDestroy() {
- eventHub.$off('enable.key', this.enableKey);
- eventHub.$off('remove.key', this.disableKey);
- eventHub.$off('disable.key', this.disableKey);
+ enableKey(deployKey) {
+ this.service
+ .enableKey(deployKey.id)
+ .then(this.fetchKeys)
+ .catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
- methods: {
- fetchKeys() {
- this.isLoading = true;
-
- this.service.getKeys()
- .then((data) => {
- this.isLoading = false;
- this.store.keys = data;
- })
- .catch(() => new Flash('Error getting deploy keys'));
- },
- enableKey(deployKey) {
- this.service.enableKey(deployKey.id)
- .then(() => this.fetchKeys())
- .catch(() => new Flash('Error enabling deploy key'));
- },
- disableKey(deployKey, callback) {
- // eslint-disable-next-line no-alert
- if (confirm('You are going to remove this deploy key. Are you sure?')) {
- this.service.disableKey(deployKey.id)
- .then(() => this.fetchKeys())
- .then(callback)
- .catch(() => new Flash('Error removing deploy key'));
- } else {
- callback();
- }
- },
+ disableKey(deployKey, callback) {
+ // eslint-disable-next-line no-alert
+ if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
+ this.service
+ .disableKey(deployKey.id)
+ .then(this.fetchKeys)
+ .then(callback)
+ .catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
+ } else {
+ callback();
+ }
},
- };
+ },
+};
</script>
<template>
@@ -82,29 +117,38 @@
<loading-icon
v-if="isLoading && !hasKeys"
size="2"
- label="Loading deploy keys"
+ :label="s__('DeployKeys|Loading deploy keys')"
/>
- <div v-else-if="hasKeys">
+ <template v-else-if="hasKeys">
+ <div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
+ <div class="fade-left">
+ <i
+ class="fa fa-angle-left"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+ <div class="fade-right">
+ <i
+ class="fa fa-angle-right"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+
+ <navigation-tabs
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
+ scope="deployKeys"
+ />
+ </div>
<keys-panel
- title="Enabled deploy keys for this project"
class="qa-project-deploy-keys"
- :keys="keys.enabled_keys"
- :store="store"
- :endpoint="endpoint"
- />
- <keys-panel
- title="Deploy keys from projects you have access to"
- :keys="keys.available_project_keys"
- :store="store"
- :endpoint="endpoint"
- />
- <keys-panel
- v-if="keys.public_keys.length"
- title="Public deploy keys available to any project"
- :keys="keys.public_keys"
+ :project-id="projectId"
+ :keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
/>
- </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index c6091efd62f..6c2af7fa768 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,111 +1,235 @@
<script>
- import actionBtn from './action_btn.vue';
- import { getTimeago } from '../../lib/utils/datetime_utility';
- import tooltip from '../../vue_shared/directives/tooltip';
+import _ from 'underscore';
+import { s__, sprintf } from '~/locale';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
- export default {
- components: {
- actionBtn,
- },
- directives: {
- tooltip,
- },
- props: {
- deployKey: {
- type: Object,
- required: true,
- },
- store: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
- },
- computed: {
- timeagoDate() {
- return getTimeago().format(this.deployKey.created_at);
- },
- editDeployKeyPath() {
- return `${this.endpoint}/${this.deployKey.id}/edit`;
- },
- },
- methods: {
- isEnabled(id) {
- return this.store.findEnabledKey(id) !== undefined;
- },
- tooltipTitle(project) {
- return project.can_push ? 'Write access allowed' : 'Read access only';
- },
- },
- };
+import actionBtn from './action_btn.vue';
+
+export default {
+ components: {
+ actionBtn,
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ deployKey: {
+ type: Object,
+ required: true,
+ },
+ store: {
+ type: Object,
+ required: true,
+ },
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ projectsExpanded: false,
+ };
+ },
+ computed: {
+ editDeployKeyPath() {
+ return `${this.endpoint}/${this.deployKey.id}/edit`;
+ },
+ projects() {
+ const projects = [...this.deployKey.deploy_keys_projects];
+
+ if (this.projectId !== null) {
+ const indexOfCurrentProject = _.findIndex(
+ projects,
+ project =>
+ project &&
+ project.project &&
+ project.project.id &&
+ project.project.id.toString() === this.projectId,
+ );
+
+ if (indexOfCurrentProject > -1) {
+ const currentProject = projects.splice(indexOfCurrentProject, 1);
+ currentProject[0].project.full_name = s__('DeployKeys|Current project');
+ return currentProject.concat(projects);
+ }
+ }
+ return projects;
+ },
+ firstProject() {
+ return _.head(this.projects);
+ },
+ restProjects() {
+ return _.tail(this.projects);
+ },
+ restProjectsTooltip() {
+ return sprintf(s__('DeployKeys|Expand %{count} other projects'), {
+ count: this.restProjects.length,
+ });
+ },
+ restProjectsLabel() {
+ return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length });
+ },
+ isEnabled() {
+ return this.store.isEnabled(this.deployKey.id);
+ },
+ isRemovable() {
+ return (
+ this.store.isEnabled(this.deployKey.id) &&
+ this.deployKey.destroyed_when_orphaned &&
+ this.deployKey.almost_orphaned
+ );
+ },
+ isExpandable() {
+ return !this.projectsExpanded && this.restProjects.length > 1;
+ },
+ isExpanded() {
+ return this.projectsExpanded || this.restProjects.length === 1;
+ },
+ },
+ methods: {
+ projectTooltipTitle(project) {
+ return project.can_push
+ ? s__('DeployKeys|Write access allowed')
+ : s__('DeployKeys|Read access only');
+ },
+ toggleExpanded() {
+ this.projectsExpanded = !this.projectsExpanded;
+ },
+ },
+};
</script>
<template>
- <div>
- <div class="pull-left append-right-10 hidden-xs">
- <i
- aria-hidden="true"
- class="fa fa-key key-icon"
- >
- </i>
+ <div class="gl-responsive-table-row deploy-key">
+ <div class="table-section section-40">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ s__('DeployKeys|Deploy key') }}
+ </div>
+ <div class="table-mobile-content">
+ <strong class="title qa-key-title">
+ {{ deployKey.title }}
+ </strong>
+ <div class="fingerprint qa-key-fingerprint">
+ {{ deployKey.fingerprint }}
+ </div>
+ </div>
</div>
- <div class="deploy-key-content key-list-item-info">
- <strong class="title qa-key-title">
- {{ deployKey.title }}
- </strong>
- <div class="description qa-key-fingerprint">
- {{ deployKey.fingerprint }}
+ <div class="table-section section-30 section-wrap">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ s__('DeployKeys|Project usage') }}
+ </div>
+ <div class="table-mobile-content deploy-project-list">
+ <template v-if="projects.length > 0">
+ <a
+ class="label deploy-project-label"
+ :title="projectTooltipTitle(firstProject)"
+ v-tooltip
+ >
+ <span>
+ {{ firstProject.project.full_name }}
+ </span>
+ <icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
+ </a>
+ <a
+ v-if="isExpandable"
+ class="label deploy-project-label"
+ @click="toggleExpanded"
+ :title="restProjectsTooltip"
+ v-tooltip
+ >
+ <span>{{ restProjectsLabel }}</span>
+ </a>
+ <a
+ v-else-if="isExpanded"
+ v-for="deployKeysProject in restProjects"
+ :key="deployKeysProject.project.full_path"
+ class="label deploy-project-label"
+ :href="deployKeysProject.project.full_path"
+ :title="projectTooltipTitle(deployKeysProject)"
+ v-tooltip
+ >
+ <span>
+ {{ deployKeysProject.project.full_name }}
+ </span>
+ <icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/>
+ </a>
+ </template>
+ <span
+ v-else
+ class="text-secondary">{{ __('None') }}</span>
</div>
</div>
- <div class="deploy-key-content prepend-left-default deploy-key-projects">
- <a
- v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects"
- :key="i"
- class="label deploy-project-label"
- :href="deployKeysProject.project.full_path"
- :title="tooltipTitle(deployKeysProject)"
- v-tooltip
- >
- {{ deployKeysProject.project.full_name }}
- <i
- v-if="!deployKeysProject.can_push"
- aria-hidden="true"
- class="fa fa-lock"
- >
- </i>
- </a>
+ <div class="table-section section-15 text-right">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ __('Created') }}
+ </div>
+ <div class="table-mobile-content text-secondary key-created-at">
+ <span
+ :title="tooltipTitle(deployKey.created_at)"
+ v-tooltip>
+ <icon name="calendar"/>
+ <span>{{ timeFormated(deployKey.created_at) }}</span>
+ </span>
+ </div>
</div>
- <div class="deploy-key-content">
- <span class="key-created-at">
- created {{ timeagoDate }}
- </span>
- <a
- v-if="deployKey.can_edit"
- class="btn btn-sm"
- :href="editDeployKeyPath"
- >
- Edit
- </a>
- <action-btn
- v-if="!isEnabled(deployKey.id)"
- :deploy-key="deployKey"
- type="enable"
- />
- <action-btn
- v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
- :deploy-key="deployKey"
- btn-css-class="btn-warning"
- type="remove"
- />
- <action-btn
- v-else
- :deploy-key="deployKey"
- btn-css-class="btn-warning"
- type="disable"
- />
+ <div class="table-section section-15 table-button-footer deploy-key-actions">
+ <div class="btn-group table-action-buttons">
+ <action-btn
+ v-if="!isEnabled"
+ :deploy-key="deployKey"
+ type="enable"
+ >
+ {{ __('Enable') }}
+ </action-btn>
+ <a
+ v-if="deployKey.can_edit"
+ class="btn btn-default text-secondary"
+ :href="editDeployKeyPath"
+ :title="__('Edit')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="pencil"/>
+ </a>
+ <action-btn
+ v-if="isRemovable"
+ :deploy-key="deployKey"
+ btn-css-class="btn-danger"
+ type="remove"
+ :title="__('Remove')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="remove"/>
+ </action-btn>
+ <action-btn
+ v-else-if="isEnabled"
+ :deploy-key="deployKey"
+ btn-css-class="btn-warning"
+ type="disable"
+ :title="__('Disable')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="cancel"/>
+ </action-btn>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
index 822b0323156..3b146c7389a 100644
--- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -1,62 +1,68 @@
<script>
- import key from './key.vue';
+import deployKey from './key.vue';
- export default {
- components: {
- key,
+export default {
+ components: {
+ deployKey,
+ },
+ props: {
+ keys: {
+ type: Array,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- keys: {
- type: Array,
- required: true,
- },
- showHelpBox: {
- type: Boolean,
- required: false,
- default: true,
- },
- store: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
+ store: {
+ type: Object,
+ required: true,
},
- };
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
</script>
<template>
- <div class="deploy-keys-panel">
- <h5>
- {{ title }}
- ({{ keys.length }})
- </h5>
- <ul
- class="well-list"
- v-if="keys.length"
- >
- <li
+ <div class="deploy-keys-panel table-holder">
+ <template v-if="keys.length > 0">
+ <div
+ role="row"
+ class="gl-responsive-table-row table-row-header">
+ <div
+ role="rowheader"
+ class="table-section section-40">
+ {{ s__('DeployKeys|Deploy key') }}
+ </div>
+ <div
+ role="rowheader"
+ class="table-section section-30">
+ {{ s__('DeployKeys|Project usage') }}
+ </div>
+ <div
+ role="rowheader"
+ class="table-section section-15 text-right">
+ {{ __('Created') }}
+ </div>
+ </div>
+ <deploy-key
v-for="deployKey in keys"
:key="deployKey.id"
- >
- <key
- :deploy-key="deployKey"
- :store="store"
- :endpoint="endpoint"
- />
- </li>
- </ul>
+ :deploy-key="deployKey"
+ :store="store"
+ :endpoint="endpoint"
+ :project-id="projectId"
+ />
+ </template>
<div
class="settings-message text-center"
- v-else-if="showHelpBox"
+ v-else
>
- No deploy keys found. Create one with the form above.
+ {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js
index b727261648c..6e439be42ae 100644
--- a/app/assets/javascripts/deploy_keys/index.js
+++ b/app/assets/javascripts/deploy_keys/index.js
@@ -1,21 +1,24 @@
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
-export default () => new Vue({
- el: document.getElementById('js-deploy-keys'),
- components: {
- deployKeysApp,
- },
- data() {
- return {
- endpoint: this.$options.el.dataset.endpoint,
- };
- },
- render(createElement) {
- return createElement('deploy-keys-app', {
- props: {
- endpoint: this.endpoint,
- },
- });
- },
-});
+export default () =>
+ new Vue({
+ el: document.getElementById('js-deploy-keys'),
+ components: {
+ deployKeysApp,
+ },
+ data() {
+ return {
+ endpoint: this.$options.el.dataset.endpoint,
+ projectId: this.$options.el.dataset.projectId,
+ };
+ },
+ render(createElement) {
+ return createElement('deploy-keys-app', {
+ props: {
+ endpoint: this.endpoint,
+ projectId: this.projectId,
+ },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js
index fe6dbaa9498..9dc3b21f6f6 100644
--- a/app/assets/javascripts/deploy_keys/service/index.js
+++ b/app/assets/javascripts/deploy_keys/service/index.js
@@ -1,34 +1,24 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class DeployKeysService {
constructor(endpoint) {
- this.endpoint = endpoint;
-
- this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
- enable: {
- method: 'PUT',
- url: `${this.endpoint}{/id}/enable`,
- },
- disable: {
- method: 'PUT',
- url: `${this.endpoint}{/id}/disable`,
- },
+ this.axios = axios.create({
+ baseURL: endpoint,
});
}
getKeys() {
- return this.resource.get()
- .then(response => response.json());
+ return this.axios.get()
+ .then(response => response.data);
}
enableKey(id) {
- return this.resource.enable({ id }, {});
+ return this.axios.put(`${id}/enable`)
+ .then(response => response.data);
}
disableKey(id) {
- return this.resource.disable({ id }, {});
+ return this.axios.put(`${id}/disable`)
+ .then(response => response.data);
}
}
diff --git a/app/assets/javascripts/deploy_keys/store/index.js b/app/assets/javascripts/deploy_keys/store/index.js
index 6210361af26..a350bc99a70 100644
--- a/app/assets/javascripts/deploy_keys/store/index.js
+++ b/app/assets/javascripts/deploy_keys/store/index.js
@@ -3,7 +3,7 @@ export default class DeployKeysStore {
this.keys = {};
}
- findEnabledKey(id) {
- return this.keys.enabled_keys.find(key => key.id === id);
+ isEnabled(id) {
+ return this.keys.enabled_keys.some(key => key.id === id);
}
}
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 180a6bd67e7..fe9b0795609 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -79,7 +79,7 @@ const DiffNoteAvatars = Vue.extend({
storeState: {
handler() {
this.$nextTick(() => {
- $('.has-tooltip', this.$el).tooltip('fixTitle');
+ $('.has-tooltip', this.$el).tooltip('_fixTitle');
// We need to add/remove a class to an element that is outside the Vue instance
this.addNoCommentClass();
@@ -138,7 +138,7 @@ const DiffNoteAvatars = Vue.extend({
this.$nextTick(() => {
this.setDiscussionVisible();
- $('.has-tooltip', this.$el).tooltip('fixTitle');
+ $('.has-tooltip', this.$el).tooltip('_fixTitle');
$('.has-tooltip', this.$el).tooltip('hide');
});
},
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index df4c72ba0ed..8d66417abac 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -61,7 +61,7 @@ const ResolveBtn = Vue.extend({
this.$nextTick(() => {
$(this.$refs.button)
.tooltip('hide')
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
});
},
resolve: function () {
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 842a4255f08..4164149dd06 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -2,7 +2,9 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
+import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
@@ -14,6 +16,7 @@ class DueDateSelect {
this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block;
+ this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
@@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
- const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
+ const hasDueDate = this.displayedDate !== 'No due date';
+ const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn();
@@ -145,10 +149,13 @@ class DueDateSelect {
return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => {
+ const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
+ this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
+
return this.$loading.fadeOut();
});
}
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index dc7672560ea..cd8dff40b88 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() {
symbols: [],
flags: [],
};
- Object.keys(emojiMap).forEach((name) => {
+ Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name);
@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) {
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
- const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
+ const fallbackSpriteAttribute = opts.sprite
+ ? `data-fallback-sprite-class="${fallbackSpriteClass}"`
+ : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js
index c18d07dad43..8c1861c56db 100644
--- a/app/assets/javascripts/emoji/support/unicode_support_map.js
+++ b/app/assets/javascripts/emoji/support/unicode_support_map.js
@@ -54,7 +54,8 @@ const unicodeSupportTestMap = {
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA
const indexOffset = 4 * pixelOffset;
- const hasColor = imageDataArray[indexOffset + 0] ||
+ const hasColor =
+ imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3];
@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
const fontSize = 16;
function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
- const numTestEntries = testMapKeys
- .reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
+ const numTestEntries = testMapKeys.reduce((list, testKey) => list.concat(testMap[testKey]), [])
+ .length;
const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d');
- canvas.width = (2 * fontSize);
- canvas.height = (numTestEntries * fontSize);
+ canvas.width = 2 * fontSize;
+ canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically
let writeIndex = 0;
- testMapKeys.forEach((testKey) => {
+ testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey];
- [].concat(testEntry).forEach((emojiUnicode) => {
- ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
+ [].concat(testEntry).forEach(emojiUnicode => {
+ ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2);
writeIndex += 1;
});
});
@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) {
// Read from the canvas
const resultMap = {};
let readIndex = 0;
- testMapKeys.forEach((testKey) => {
+ testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries
- const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
+ const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => {
// Sample along the vertical-middle for a couple of characters
- const imageData = ctx.getImageData(
- 0,
- (readIndex * fontSize) + (fontSize / 2),
- 2 * fontSize,
- 1,
- ).data;
+ const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1)
+ .data;
let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize;
- const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
+ const isLookingAtSecondChar = currentPixel >= fontSize + fontSize / 2;
// Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true;
- // Check to see that nothing is rendered next to the first character
- // to ensure that the ZWJ sequence rendered as one piece
+ // Check to see that nothing is rendered next to the first character
+ // to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false;
break;
@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() {
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
- window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
+ window.localStorage.setItem(
+ 'gl-emoji-unicode-support-map',
+ JSON.stringify(unicodeSupportMap),
+ );
}
}
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index dbee81fa320..6bd7c6b49cb 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -43,6 +43,7 @@
<div class="environments-container">
<loading-icon
+ class="prepend-top-default"
label="Loading environments"
v-if="isLoading"
size="3"
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 16bd2f5feb3..0b3fef9fcca 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,5 +1,5 @@
<script>
- import playIconSvg from 'icons/_icon_play.svg';
+ import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -8,9 +8,9 @@
directives: {
tooltip,
},
-
components: {
loadingIcon,
+ Icon,
},
props: {
actions: {
@@ -19,20 +19,16 @@
default: () => [],
},
},
-
data() {
return {
- playIconSvg,
isLoading: false,
};
},
-
computed: {
title() {
return 'Deploy to...';
},
},
-
methods: {
onClickAction(endpoint) {
this.isLoading = true;
@@ -65,7 +61,10 @@
:disabled="isLoading"
>
<span>
- <span v-html="playIconSvg"></span>
+ <icon
+ name="play"
+ :size="12"
+ />
<i
class="fa fa-caret-down"
aria-hidden="true"
@@ -75,7 +74,7 @@
</span>
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(action, i) in actions"
:key="i">
@@ -86,7 +85,10 @@
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
>
- <span v-html="playIconSvg"></span>
+ <icon
+ name="play"
+ :size="12"
+ />
<span>
{{ action.name }}
</span>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index c9a68cface6..ea6f1168c68 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,4 +1,5 @@
<script>
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
@@ -6,6 +7,9 @@
* Renders the external url link in environments table.
*/
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
@@ -15,7 +19,6 @@
required: true,
},
},
-
computed: {
title() {
return s__('Environments|Open');
@@ -34,10 +37,9 @@
:aria-label="title"
:href="externalUrl"
>
- <i
- class="fa fa-external-link"
- aria-hidden="true"
- >
- </i>
+ <icon
+ name="external-link"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 79326ca3487..23aaab2c441 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -486,14 +486,14 @@
{{ model.folderName }}
</span>
- <span class="badge">
+ <span class="badge badge-pill">
{{ model.size }}
</span>
</span>
</div>
<div
- class="table-section section-10 deployment-column hidden-xs hidden-sm"
+ class="table-section section-10 deployment-column d-none d-sm-none d-md-block"
role="gridcell"
>
<span v-if="shouldRenderDeploymentID">
@@ -513,7 +513,7 @@
</div>
<div
- class="table-section section-15 hidden-xs hidden-sm"
+ class="table-section section-15 d-none d-sm-none d-md-block"
role="gridcell"
>
<a
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 081537cf218..8df1b6317e3 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,20 +2,22 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
-
props: {
monitoringUrl: {
type: String,
required: true,
},
},
-
computed: {
title() {
return 'Monitoring';
@@ -26,17 +28,16 @@
<template>
<a
v-tooltip
- class="btn monitoring-url hidden-xs hidden-sm"
+ class="btn monitoring-url d-none d-sm-none d-md-block"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
:aria-label="title"
>
- <i
- class="fa fa-area-chart"
- aria-hidden="true"
- >
- </i>
+ <icon
+ name="chart"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 605a88e997e..7515d711c50 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -12,7 +12,6 @@
components: {
loadingIcon,
},
-
props: {
retryUrl: {
type: String,
@@ -24,13 +23,11 @@
default: true,
},
},
-
data() {
return {
isLoading: false,
};
},
-
methods: {
onClick() {
this.isLoading = true;
@@ -43,7 +40,7 @@
<template>
<button
type="button"
- class="btn hidden-xs hidden-sm"
+ class="btn d-none d-sm-none d-md-block"
@click="onClick"
:disabled="isLoading"
>
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index dda7429a726..7055f208451 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -43,7 +43,7 @@
if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
- $(this.$el).tooltip('destroy');
+ $(this.$el).tooltip('dispose');
eventHub.$emit('postAction', this.stopUrl);
}
@@ -55,7 +55,7 @@
<button
v-tooltip
type="button"
- class="btn stop-env-link hidden-xs hidden-sm"
+ class="btn stop-env-link d-none d-sm-none d-md-block"
data-container="body"
@click="onClick"
:disabled="isLoading"
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 407d5333c0e..0dbbbb75e07 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -3,14 +3,16 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
- import terminalIconSvg from 'icons/_icon_terminal.svg';
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
-
props: {
terminalPath: {
type: String,
@@ -18,13 +20,6 @@
default: '',
},
},
-
- data() {
- return {
- terminalIconSvg,
- };
- },
-
computed: {
title() {
return 'Terminal';
@@ -35,12 +30,15 @@
<template>
<a
v-tooltip
- class="btn terminal-button hidden-xs hidden-sm"
+ class="btn terminal-button d-none d-sm-none d-md-block"
data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath"
- v-html="terminalIconSvg"
>
+ <icon
+ name="terminal"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index c0be72f7401..3da762446c9 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -68,8 +68,7 @@
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
this.service.getFolderContent(folder.folder_path)
- .then(resp => resp.json())
- .then(response => this.store.setfolderContent(folder, response.environments))
+ .then(response => this.store.setfolderContent(folder, response.data.environments))
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
.catch(() => {
Flash(s__('Environments|An error occurred while fetching the environments.'));
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 34d18d55120..a7a79dbca70 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -6,7 +6,6 @@ import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import {
getParameterByName,
- parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
import Flash from '../../flash';
@@ -46,17 +45,14 @@ export default {
methods: {
saveData(resp) {
- const headers = resp.headers;
- return resp.json().then((response) => {
- this.isLoading = false;
-
- if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
- this.store.storeAvailableCount(response.available_count);
- this.store.storeStoppedCount(response.stopped_count);
- this.store.storeEnvironments(response.environments);
- this.store.setPagination(headers);
- }
- });
+ this.isLoading = false;
+
+ if (_.isEqual(resp.config.params, this.requestData)) {
+ this.store.storeAvailableCount(resp.data.available_count);
+ this.store.storeStoppedCount(resp.data.stopped_count);
+ this.store.storeEnvironments(resp.data.environments);
+ this.store.setPagination(resp.headers);
+ }
},
/**
@@ -70,7 +66,7 @@ export default {
updateContent(parameters) {
this.updateInternalState(parameters);
// fetch new data
- return this.service.get(this.requestData)
+ return this.service.fetchEnvironments(this.requestData)
.then(response => this.successCallback(response))
.then(() => {
// restart polling
@@ -105,7 +101,7 @@ export default {
fetchEnvironments() {
this.isLoading = true;
- return this.service.get(this.requestData)
+ return this.service.fetchEnvironments(this.requestData)
.then(this.successCallback)
.catch(this.errorCallback);
},
@@ -141,7 +137,7 @@ export default {
this.poll = new Poll({
resource: this.service,
- method: 'get',
+ method: 'fetchEnvironments',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 03ab74b3338..3b121551aca 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -1,25 +1,22 @@
-/* eslint-disable class-methods-use-this */
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class EnvironmentsService {
constructor(endpoint) {
- this.environments = Vue.resource(endpoint);
+ this.environmentsEndpoint = endpoint;
this.folderResults = 3;
}
- get(options = {}) {
+ fetchEnvironments(options = {}) {
const { scope, page } = options;
- return this.environments.get({ scope, page });
+ return axios.get(this.environmentsEndpoint, { params: { scope, page } });
}
+ // eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
- return Vue.http.post(endpoint, {}, { emulateJSON: true });
+ return axios.post(endpoint, {}, { emulateJSON: true });
}
getFolderContent(folderUrl) {
- return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`);
+ return axios.get(`${folderUrl}.json?per_page=${this.folderResults}`);
}
}
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
index 2d5bae9a9c4..2f27c9351bc 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -24,7 +24,7 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
template: `
<div class="popover feature-highlight-popover" role="tooltip">
<div class="arrow"></div>
- <div class="popover-content"></div>
+ <div class="popover-body"></div>
</div>
`,
})
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 7e9770a9ea2..9de57db48fd 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -408,7 +408,10 @@ class GfmAutoComplete {
fetchData($input, at) {
if (this.isLoadingData[at]) return;
+
this.isLoadingData[at] = true;
+ const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
+
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
@@ -418,12 +421,14 @@ class GfmAutoComplete {
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
.catch(() => { this.isLoadingData[at] = false; });
- } else {
- AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
+ } else if (dataSource) {
+ AjaxCache.retrieve(dataSource, true)
.then((data) => {
this.loadData($input, at, data);
})
.catch(() => { this.isLoadingData[at] = false; });
+ } else {
+ this.isLoadingData[at] = false;
}
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index fa48d7d1915..746a06b7c4f 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -374,7 +374,7 @@ GitLabDropdown = (function() {
$relatedTarget = $(e.relatedTarget);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu');
if ($dropdownMenu.length === 0) {
- return _this.dropdown.removeClass('open');
+ return _this.dropdown.removeClass('show');
}
}
};
@@ -801,7 +801,7 @@ GitLabDropdown = (function() {
if (this.options.filterable) {
const initialScrollTop = $(window).scrollTop();
- if (this.dropdown.is('.open')) {
+ if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) {
this.filterInput.focus();
}
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
index 972b2252acb..87c6e37b9fb 100644
--- a/app/assets/javascripts/gl_field_error.js
+++ b/app/assets/javascripts/gl_field_error.js
@@ -62,7 +62,7 @@ export default class GlFieldError {
this.inputDomElement = this.inputElement.get(0);
this.form = formErrors;
this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
- this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);
+ this.fieldErrorElement = $(`<p class='${errorMessageClass} hidden'>${this.errorMessage}</p>`);
this.state = {
valid: false,
@@ -146,8 +146,8 @@ export default class GlFieldError {
renderInvalid() {
this.inputElement.addClass(inputErrorClass);
- this.scopedSiblings.hide();
- return this.fieldErrorElement.show();
+ this.scopedSiblings.addClass('hidden');
+ return this.fieldErrorElement.removeClass('hidden');
}
renderClear() {
@@ -157,7 +157,7 @@ export default class GlFieldError {
this.accessCurrentValue(trimmedInput);
}
this.inputElement.removeClass(inputErrorClass);
- this.scopedSiblings.hide();
- this.fieldErrorElement.hide();
+ this.scopedSiblings.addClass('hidden');
+ this.fieldErrorElement.addClass('hidden');
}
}
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index 502e3569321..029fd6a67d4 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -7,12 +7,12 @@ import { __ } from '~/locale';
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
- const form = $('.commits-search-form');
+ const tag = $('.js-signature-container');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
- const params = parseQueryStringIntoObject(form.serialize());
- return axios.get(form.data('signaturesPath'), { params })
+ const params = parseQueryStringIntoObject(tag.serialize());
+ return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => {
data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 764b130fdb8..7f64a9bd741 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -99,7 +99,7 @@ export default {
/>
</div>
<div
- class="avatar-container prepend-top-8 prepend-left-5 s24 hidden-xs"
+ class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block"
:class="{ 'content-loading': group.isChildrenLoading }"
>
<a
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
new file mode 100644
index 00000000000..05dbc1410de
--- /dev/null
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -0,0 +1,106 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { activityBarViews } from '../constants';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ computed: {
+ ...mapGetters(['currentProject', 'hasChanges']),
+ ...mapState(['currentActivityView']),
+ goBackUrl() {
+ return document.referrer || this.currentProject.web_url;
+ },
+ },
+ methods: {
+ ...mapActions(['updateActivityBarView']),
+ },
+ activityBarViews,
+};
+</script>
+
+<template>
+ <nav class="ide-activity-bar">
+ <ul class="list-unstyled">
+ <li v-once>
+ <a
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ :href="goBackUrl"
+ class="ide-sidebar-link"
+ :title="s__('IDE|Go back')"
+ :aria-label="s__('IDE|Go back')"
+ >
+ <icon
+ :size="16"
+ name="go-back"
+ />
+ </a>
+ </li>
+ <li>
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-edit-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.edit
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.edit)"
+ :title="s__('IDE|Edit')"
+ :aria-label="s__('IDE|Edit')"
+ >
+ <icon
+ name="code"
+ />
+ </button>
+ </li>
+ <li>
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-review-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.review
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.review)"
+ :title="s__('IDE|Review')"
+ :aria-label="s__('IDE|Review')"
+ >
+ <icon
+ name="file-modified"
+ />
+ </button>
+ </li>
+ <li v-show="hasChanges">
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-commit-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.commit
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.commit)"
+ :title="s__('IDE|Commit')"
+ :aria-label="s__('IDE|Commit')"
+ >
+ <icon
+ name="commit"
+ />
+ </button>
+ </li>
+ </ul>
+ </nav>
+</template>
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 037e3efb4ce..1cec84706fc 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,31 +1,88 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import { pluralize } from '~/lib/utils/text_utility';
+import { __, sprintf } from '~/locale';
export default {
components: {
- icon,
+ Icon,
+ },
+ directives: {
+ tooltip,
},
props: {
file: {
type: Object,
required: true,
},
+ showTooltip: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showStagedIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ forceModifiedIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
changedIcon() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
+ const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
+ return this.file.tempFile && !this.forceModifiedIcon
+ ? `file-addition${suffix}`
+ : `file-modified${suffix}`;
+ },
+ stagedIcon() {
+ return `${this.changedIcon}-solid`;
},
changedIconClass() {
- return `multi-${this.changedIcon}`;
+ return `multi-${this.changedIcon} pull-left`;
+ },
+ tooltipTitle() {
+ if (!this.showTooltip) return undefined;
+
+ const type = this.file.tempFile ? 'addition' : 'modification';
+
+ if (this.file.changed && !this.file.staged) {
+ return sprintf(__('Unstaged %{type}'), {
+ type,
+ });
+ } else if (!this.file.changed && this.file.staged) {
+ return sprintf(__('Staged %{type}'), {
+ type,
+ });
+ } else if (this.file.changed && this.file.staged) {
+ return sprintf(__('Unstaged and staged %{type}'), {
+ type: pluralize(type),
+ });
+ }
+
+ return undefined;
},
},
};
</script>
<template>
- <icon
- :name="changedIcon"
- :size="12"
- :css-classes="`ide-file-changed-icon ${changedIconClass}`"
- />
+ <span
+ v-tooltip
+ :title="tooltipTitle"
+ data-container="body"
+ data-placement="right"
+ class="ide-file-changed-icon"
+ >
+ <icon
+ v-if="file.changed || file.tempFile || file.staged"
+ :name="changedIcon"
+ :size="12"
+ :css-classes="changedIconClass"
+ />
+ </span>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 45321df191c..b4f3778d946 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
@@ -9,7 +9,8 @@ export default {
RadioGroup,
},
computed: {
- ...mapState(['currentBranchId']),
+ ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
+ ...mapGetters(['currentProject']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
@@ -17,6 +18,17 @@ export default {
false,
);
},
+ disableMergeRequestRadio() {
+ return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
+ },
+ },
+ mounted() {
+ if (this.disableMergeRequestRadio) {
+ this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
+ }
+ },
+ methods: {
+ ...mapActions('commit', ['updateCommitAction']),
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
@@ -41,9 +53,11 @@ export default {
:show-input="true"
/>
<radio-group
+ v-if="currentProject.merge_requests_enabled"
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
+ :disabled="disableMergeRequestRadio"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
new file mode 100644
index 00000000000..d0a60d647e5
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
@@ -0,0 +1,36 @@
+<script>
+import { mapState } from 'vuex';
+
+export default {
+ computed: {
+ ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="!lastCommitMsg"
+ class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
+ >
+ <div
+ class="ide-commit-empty-state-container"
+ >
+ <div class="svg-content svg-80">
+ <img :src="noChangesStateSvgPath" />
+ </div>
+ <div class="append-right-default prepend-left-default">
+ <div
+ class="text-content text-center"
+ >
+ <h4>
+ {{ __('No changes') }}
+ </h4>
+ <p>
+ {{ __('Edit files in the editor and commit changes here') }}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
new file mode 100644
index 00000000000..81961fe3c57
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -0,0 +1,171 @@
+<script>
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { sprintf, __ } from '~/locale';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import CommitMessageField from './message_field.vue';
+import Actions from './actions.vue';
+import SuccessMessage from './success_message.vue';
+import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
+
+export default {
+ components: {
+ Actions,
+ LoadingButton,
+ CommitMessageField,
+ SuccessMessage,
+ },
+ data() {
+ return {
+ isCompact: true,
+ componentHeight: null,
+ };
+ },
+ computed: {
+ ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
+ ...mapGetters(['hasChanges']),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ overviewText() {
+ return sprintf(
+ __(
+ '<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
+ ),
+ {
+ stagedFilesLength: this.stagedFiles.length,
+ changedFilesLength: this.changedFiles.length,
+ },
+ );
+ },
+ },
+ watch: {
+ currentActivityView() {
+ if (this.lastCommitMsg) {
+ this.isCompact = false;
+ } else {
+ this.isCompact = !(
+ this.currentActivityView === activityBarViews.commit &&
+ window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
+ );
+ }
+ },
+ lastCommitMsg() {
+ this.isCompact =
+ this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
+ },
+ },
+ methods: {
+ ...mapActions(['updateActivityBarView']),
+ ...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
+ toggleIsSmall() {
+ this.updateActivityBarView(activityBarViews.commit)
+ .then(() => {
+ this.isCompact = !this.isCompact;
+ })
+ .catch(e => {
+ throw e;
+ });
+ },
+ beforeEnterTransition() {
+ const elHeight = this.isCompact
+ ? this.$refs.formEl && this.$refs.formEl.offsetHeight
+ : this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
+
+ this.componentHeight = elHeight;
+ },
+ enterTransition() {
+ this.$nextTick(() => {
+ const elHeight = this.isCompact
+ ? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
+ : this.$refs.formEl && this.$refs.formEl.offsetHeight;
+
+ this.componentHeight = elHeight;
+ });
+ },
+ afterEndTransition() {
+ this.componentHeight = null;
+ },
+ },
+ activityBarViews,
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-form"
+ :class="{
+ 'is-compact': isCompact,
+ 'is-full': !isCompact
+ }"
+ :style="{
+ height: componentHeight ? `${componentHeight}px` : null,
+ }"
+ >
+ <transition
+ name="commit-form-slide-up"
+ @before-enter="beforeEnterTransition"
+ @enter="enterTransition"
+ @after-enter="afterEndTransition"
+ >
+ <div
+ v-if="isCompact"
+ class="commit-form-compact"
+ ref="compactEl"
+ >
+ <button
+ type="button"
+ :disabled="!hasChanges"
+ class="btn btn-primary btn-sm btn-block"
+ @click="toggleIsSmall"
+ >
+ {{ __('Commit') }}
+ </button>
+ <p
+ class="text-center"
+ v-html="overviewText"
+ ></p>
+ </div>
+ <form
+ v-if="!isCompact"
+ class="form-horizontal"
+ @submit.prevent.stop="commitChanges"
+ ref="formEl"
+ >
+ <transition name="fade">
+ <success-message
+ v-show="lastCommitMsg"
+ />
+ </transition>
+ <commit-message-field
+ :text="commitMessage"
+ @input="updateCommitMessage"
+ />
+ <div class="clearfix prepend-top-15">
+ <actions />
+ <loading-button
+ :loading="submitCommitLoading"
+ :disabled="commitButtonDisabled"
+ container-class="btn btn-success btn-sm pull-left"
+ :label="__('Commit')"
+ @click="commitChanges"
+ />
+ <button
+ v-if="!discardDraftButtonDisabled"
+ type="button"
+ class="btn btn-default btn-sm pull-right"
+ @click="discardDraft"
+ >
+ {{ __('Discard draft') }}
+ </button>
+ <button
+ v-else
+ type="button"
+ class="btn btn-default btn-sm pull-right"
+ @click="toggleIsSmall"
+ >
+ {{ __('Collapse') }}
+ </button>
+ </div>
+ </form>
+ </transition>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 453208f3f19..c3ac18bfb83 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,66 +1,128 @@
<script>
- import { mapState } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import listItem from './list_item.vue';
- import listCollapsed from './list_collapsed.vue';
+import { mapActions } from 'vuex';
+import { __, sprintf } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import ListItem from './list_item.vue';
- export default {
- components: {
- icon,
- listItem,
- listCollapsed,
+export default {
+ components: {
+ Icon,
+ ListItem,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- fileList: {
- type: Array,
- required: true,
- },
+ fileList: {
+ type: Array,
+ required: true,
},
- computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- ]),
- isCommitInfoShown() {
- return this.rightPanelCollapsed || this.fileList.length;
- },
+ iconName: {
+ type: String,
+ required: true,
},
- methods: {
- toggleCollapsed() {
- this.$emit('toggleCollapsed');
- },
+ action: {
+ type: String,
+ required: true,
},
- };
+ actionBtnText: {
+ type: String,
+ required: true,
+ },
+ itemActionComponent: {
+ type: String,
+ required: true,
+ },
+ stagedList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ showActionButton: false,
+ };
+ },
+ computed: {
+ titleText() {
+ return sprintf(__('%{title} changes'), {
+ title: this.title,
+ });
+ },
+ },
+ methods: {
+ ...mapActions(['stageAllChanges', 'unstageAllChanges']),
+ actionBtnClicked() {
+ this[this.action]();
+ },
+ setShowActionButton(show) {
+ this.showActionButton = show;
+ },
+ },
+};
</script>
<template>
<div
- :class="{
- 'multi-file-commit-list': isCommitInfoShown
- }"
+ class="ide-commit-list-container"
>
- <list-collapsed
- v-if="rightPanelCollapsed"
- />
- <template v-else>
- <ul
- v-if="fileList.length"
- class="list-unstyled append-bottom-0"
+ <header
+ class="multi-file-commit-panel-header"
+ @mouseenter="setShowActionButton(true)"
+ @mouseleave="setShowActionButton(false)"
+ >
+ <div
+ class="multi-file-commit-panel-header-title"
>
- <li
- v-for="file in fileList"
- :key="file.key"
+ <icon
+ v-once
+ :name="iconName"
+ :size="18"
+ />
+ {{ titleText }}
+ <span
+ v-show="!showActionButton"
+ class="ide-commit-file-count"
>
- <list-item
- :file="file"
- />
- </li>
- </ul>
- </template>
+ {{ fileList.length }}
+ </span>
+ <button
+ v-show="showActionButton"
+ type="button"
+ class="btn btn-blank btn-link ide-staged-action-btn"
+ @click="actionBtnClicked"
+ >
+ {{ actionBtnText }}
+ </button>
+ </div>
+ </header>
+ <ul
+ v-if="fileList.length"
+ class="multi-file-commit-list list-unstyled append-bottom-0"
+ >
+ <li
+ v-for="file in fileList"
+ :key="file.key"
+ >
+ <list-item
+ :file="file"
+ :action-component="itemActionComponent"
+ :key-prefix="title"
+ :staged-list="stagedList"
+ />
+ </li>
+ </ul>
+ <p
+ v-else
+ class="multi-file-commit-list help-block"
+ >
+ {{ __('No changes') }}
+ </p>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 15918ac9631..2254271c679 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -1,35 +1,110 @@
<script>
- import { mapGetters } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { sprintf, n__, __ } from '~/locale';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ files: {
+ type: Array,
+ required: true,
},
- computed: {
- ...mapGetters([
- 'addedFiles',
- 'modifiedFiles',
- ]),
+ iconName: {
+ type: String,
+ required: true,
},
- };
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ addedFilesLength() {
+ return this.files.filter(f => f.tempFile).length;
+ },
+ modifiedFilesLength() {
+ return this.files.filter(f => !f.tempFile).length;
+ },
+ addedFilesIconClass() {
+ return this.addedFilesLength ? 'multi-file-addition' : '';
+ },
+ modifiedFilesClass() {
+ return this.modifiedFilesLength ? 'multi-file-modified' : '';
+ },
+ additionsTooltip() {
+ return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
+ type: this.title.toLowerCase(),
+ });
+ },
+ modifiedTooltip() {
+ return sprintf(
+ n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
+ { type: this.title.toLowerCase() },
+ );
+ },
+ titleTooltip() {
+ return sprintf(__('%{title} changes'), { title: this.title });
+ },
+ additionIconName() {
+ return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
+ },
+ modifiedIconName() {
+ return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
+ },
+ },
+};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
- <icon
- name="file-addition"
- :size="18"
- css-classes="multi-file-addition append-bottom-10"
- />
- {{ addedFiles.length }}
- <icon
- name="file-modified"
- :size="18"
- css-classes="multi-file-modified prepend-top-10 append-bottom-10"
- />
- {{ modifiedFiles.length }}
+ <div
+ v-tooltip
+ :title="titleTooltip"
+ data-container="body"
+ data-placement="left"
+ class="append-bottom-15"
+ >
+ <icon
+ v-once
+ :name="iconName"
+ :size="18"
+ />
+ </div>
+ <div
+ v-tooltip
+ :title="additionsTooltip"
+ data-container="body"
+ data-placement="left"
+ class="append-bottom-10"
+ >
+ <icon
+ :name="additionIconName"
+ :size="18"
+ :css-classes="addedFilesIconClass"
+ />
+ </div>
+ {{ addedFilesLength }}
+ <div
+ v-tooltip
+ :title="modifiedTooltip"
+ data-container="body"
+ data-placement="left"
+ class="prepend-top-10 append-bottom-10"
+ >
+ <icon
+ :name="modifiedIconName"
+ :size="18"
+ :css-classes="modifiedFilesClass"
+ />
+ </div>
+ {{ modifiedFilesLength }}
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 560cdd941cd..03f3e4de83c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,34 +1,70 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
+import StageButton from './stage_button.vue';
+import UnstageButton from './unstage_button.vue';
+import { viewerTypes } from '../../constants';
export default {
components: {
Icon,
+ StageButton,
+ UnstageButton,
},
props: {
file: {
type: Object,
required: true,
},
+ actionComponent: {
+ type: String,
+ required: true,
+ },
+ keyPrefix: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ stagedList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
iconName() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
+ const prefix = this.stagedList ? '-solid' : '';
+ return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
},
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
},
methods: {
- ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
- openFileInEditor(file) {
- return this.openPendingTab(file).then(changeViewer => {
+ ...mapActions([
+ 'discardFileChanges',
+ 'updateViewer',
+ 'openPendingTab',
+ 'unstageChange',
+ 'stageChange',
+ ]),
+ openFileInEditor() {
+ return this.openPendingTab({
+ file: this.file,
+ keyPrefix: this.keyPrefix.toLowerCase(),
+ }).then(changeViewer => {
if (changeViewer) {
- this.updateViewer('diff');
+ this.updateViewer(viewerTypes.diff);
}
});
},
+ fileAction() {
+ if (this.file.staged) {
+ this.unstageChange(this.file.path);
+ } else {
+ this.stageChange(this.file.path);
+ }
+ },
},
};
</script>
@@ -38,7 +74,9 @@ export default {
<button
type="button"
class="multi-file-commit-list-path"
- @click="openFileInEditor(file)">
+ @dblclick="fileAction"
+ @click="openFileInEditor"
+ >
<span class="multi-file-commit-list-file-path">
<icon
:name="iconName"
@@ -47,12 +85,9 @@ export default {
/>{{ file.path }}
</span>
</button>
- <button
- type="button"
- class="btn btn-blank multi-file-discard-btn"
- @click="discardFileChanges(file.path)"
- >
- Discard
- </button>
+ <component
+ :is="actionComponent"
+ :path="file.path"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index b660a2961cb..00f2312ae51 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
+import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -26,10 +27,20 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
+ tooltipTitle() {
+ return this.disabled
+ ? __('This option is disabled while you still have unstaged changes')
+ : '';
+ },
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
@@ -39,19 +50,28 @@ export default {
<template>
<fieldset>
- <label>
+ <label
+ v-tooltip
+ :title="tooltipTitle"
+ :class="{
+ 'is-disabled': disabled
+ }"
+ >
<input
type="radio"
name="commit-action"
:value="value"
@change="updateCommitAction($event.target.value)"
- :checked="checked"
- v-once
+ :checked="commitAction === value"
+ :disabled="disabled"
/>
<span class="prepend-left-10">
- <template v-if="label">
+ <span
+ v-if="label"
+ class="ide-radio-label"
+ >
{{ label }}
- </template>
+ </span>
<slot v-else></slot>
</span>
</label>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
new file mode 100644
index 00000000000..52dce8412ab
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -0,0 +1,59 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['stageChange', 'discardFileChanges']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-once
+ class="multi-file-discard-btn"
+ >
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank append-right-5"
+ :aria-label="__('Stage changes')"
+ :title="__('Stage changes')"
+ data-container="body"
+ @click.stop="stageChange(path)"
+ >
+ <icon
+ name="mobile-issue-close"
+ :size="12"
+ />
+ </button>
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank"
+ :aria-label="__('Discard changes')"
+ :title="__('Discard changes')"
+ data-container="body"
+ @click.stop="discardFileChanges(path)"
+ >
+ <icon
+ name="remove"
+ :size="12"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
new file mode 100644
index 00000000000..a6df91b79c2
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
@@ -0,0 +1,33 @@
+<script>
+import { mapState } from 'vuex';
+
+export default {
+ computed: {
+ ...mapState(['lastCommitMsg', 'committedStateSvgPath']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel-success-message"
+ aria-live="assertive"
+ >
+ <div class="svg-content svg-80">
+ <img
+ :src="committedStateSvgPath"
+ alt=""
+ />
+ </div>
+ <div class="append-right-default prepend-left-default">
+ <div
+ class="text-content text-center"
+ >
+ <h4>
+ {{ __('All changes are committed') }}
+ </h4>
+ <p v-html="lastCommitMsg"></p>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
new file mode 100644
index 00000000000..123d60da47e
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -0,0 +1,45 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['unstageChange']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-once
+ class="multi-file-discard-btn"
+ >
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank"
+ :aria-label="__('Unstage changes')"
+ :title="__('Unstage changes')"
+ data-container="body"
+ @click="unstageChange(path)"
+ >
+ <icon
+ name="history"
+ :size="12"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 0c44a755f56..b9af4d27145 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,28 +1,15 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
+import { viewerTypes } from '../constants';
export default {
- components: {
- Icon,
- },
props: {
- hasChanges: {
- type: Boolean,
- required: false,
- default: false,
- },
- mergeRequestId: {
- type: String,
- required: false,
- default: '',
- },
viewer: {
type: String,
required: true,
},
- showShadow: {
- type: Boolean,
+ mergeRequestId: {
+ type: Number,
required: true,
},
},
@@ -38,84 +25,45 @@ export default {
this.$emit('click', mode);
},
},
+ viewerTypes,
};
</script>
<template>
<div
class="dropdown"
- :class="{
- shadow: showShadow,
- }"
>
<button
type="button"
- class="btn btn-primary btn-sm"
- :class="{
- 'btn-inverted': hasChanges,
- }"
+ class="btn btn-link"
data-toggle="dropdown"
>
- <template v-if="viewer === 'mrdiff' && mergeRequestId">
- {{ mergeReviewLine }}
- </template>
- <template v-else-if="viewer === 'editor'">
- {{ __('Editing') }}
- </template>
- <template v-else>
- {{ __('Reviewing') }}
- </template>
- <icon
- name="angle-down"
- :size="12"
- css-classes="caret-down"
- />
+ {{ __('Edit') }}
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
- <template v-if="mergeRequestId">
- <li>
- <a
- href="#"
- @click.prevent="changeMode('mrdiff')"
- :class="{
- 'is-active': viewer === 'mrdiff',
- }"
- >
- <strong class="dropdown-menu-inner-title">
- {{ mergeReviewLine }}
- </strong>
- <span class="dropdown-menu-inner-content">
- {{ __('Compare changes with the merge request target branch') }}
- </span>
- </a>
- </li>
- <li
- role="separator"
- class="divider"
- >
- </li>
- </template>
<li>
<a
href="#"
- @click.prevent="changeMode('editor')"
+ @click.prevent="changeMode($options.viewerTypes.mr)"
:class="{
- 'is-active': viewer === 'editor',
+ 'is-active': viewer === $options.viewerTypes.mr,
}"
>
- <strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
+ <strong class="dropdown-menu-inner-title">
+ {{ mergeReviewLine }}
+ </strong>
<span class="dropdown-menu-inner-content">
- {{ __('View and edit lines') }}
+ {{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li>
<a
href="#"
- @click.prevent="changeMode('diff')"
+ @click.prevent="changeMode($options.viewerTypes.diff)"
:class="{
- 'is-active': viewer === 'diff',
+ 'is-active': viewer === $options.viewerTypes.diff,
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
new file mode 100644
index 00000000000..cabb3f59b17
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/index.vue
@@ -0,0 +1,243 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import VirtualList from 'vue-virtual-scroll-list';
+import Item from './item.vue';
+import router from '../../ide_router';
+import {
+ MAX_FILE_FINDER_RESULTS,
+ FILE_FINDER_ROW_HEIGHT,
+ FILE_FINDER_EMPTY_ROW_HEIGHT,
+} from '../../constants';
+import {
+ UP_KEY_CODE,
+ DOWN_KEY_CODE,
+ ENTER_KEY_CODE,
+ ESC_KEY_CODE,
+} from '../../../lib/utils/keycodes';
+
+export default {
+ components: {
+ Item,
+ VirtualList,
+ },
+ data() {
+ return {
+ focusedIndex: 0,
+ searchText: '',
+ mouseOver: false,
+ cancelMouseOver: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['allBlobs']),
+ ...mapState(['fileFindVisible', 'loading']),
+ filteredBlobs() {
+ const searchText = this.searchText.trim();
+
+ if (searchText === '') {
+ return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
+ }
+
+ return fuzzaldrinPlus.filter(this.allBlobs, searchText, {
+ key: 'path',
+ maxResults: MAX_FILE_FINDER_RESULTS,
+ });
+ },
+ filteredBlobsLength() {
+ return this.filteredBlobs.length;
+ },
+ listShowCount() {
+ return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
+ },
+ listHeight() {
+ return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
+ },
+ showClearInputButton() {
+ return this.searchText.trim() !== '';
+ },
+ },
+ watch: {
+ fileFindVisible() {
+ this.$nextTick(() => {
+ if (!this.fileFindVisible) {
+ this.searchText = '';
+ } else {
+ this.focusedIndex = 0;
+
+ if (this.$refs.searchInput) {
+ this.$refs.searchInput.focus();
+ }
+ }
+ });
+ },
+ searchText() {
+ this.focusedIndex = 0;
+ },
+ focusedIndex() {
+ if (!this.mouseOver) {
+ this.$nextTick(() => {
+ const el = this.$refs.virtualScrollList.$el;
+ const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
+ const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
+
+ if (this.focusedIndex === 0) {
+ // if index is the first index, scroll straight to start
+ el.scrollTop = 0;
+ } else if (this.focusedIndex === this.filteredBlobsLength - 1) {
+ // if index is the last index, scroll to the end
+ el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop >= bottom + el.scrollTop) {
+ // if element is off the bottom of the scroll list, scroll down one item
+ el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop < el.scrollTop) {
+ // if element is off the top of the scroll list, scroll up one item
+ el.scrollTop = scrollTop;
+ }
+ });
+ }
+ },
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ clearSearchInput() {
+ this.searchText = '';
+
+ this.$nextTick(() => {
+ this.$refs.searchInput.focus();
+ });
+ },
+ onKeydown(e) {
+ switch (e.keyCode) {
+ case UP_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex > 0) {
+ this.focusedIndex -= 1;
+ } else {
+ this.focusedIndex = this.filteredBlobsLength - 1;
+ }
+ break;
+ case DOWN_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex < this.filteredBlobsLength - 1) {
+ this.focusedIndex += 1;
+ } else {
+ this.focusedIndex = 0;
+ }
+ break;
+ default:
+ break;
+ }
+ },
+ onKeyup(e) {
+ switch (e.keyCode) {
+ case ENTER_KEY_CODE:
+ this.openFile(this.filteredBlobs[this.focusedIndex]);
+ break;
+ case ESC_KEY_CODE:
+ this.toggleFileFinder(false);
+ break;
+ default:
+ break;
+ }
+ },
+ openFile(file) {
+ this.toggleFileFinder(false);
+ router.push(`/project${file.url}`);
+ },
+ onMouseOver(index) {
+ if (!this.cancelMouseOver) {
+ this.mouseOver = true;
+ this.focusedIndex = index;
+ }
+ },
+ onMouseMove(index) {
+ this.cancelMouseOver = false;
+ this.onMouseOver(index);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-finder-overlay"
+ @mousedown.self="toggleFileFinder(false)"
+ >
+ <div
+ class="dropdown-menu diff-file-changes ide-file-finder show"
+ >
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ :placeholder="__('Search files')"
+ autocomplete="off"
+ v-model="searchText"
+ ref="searchInput"
+ @keydown="onKeydown($event)"
+ @keyup="onKeyup($event)"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ :class="{
+ hidden: showClearInputButton
+ }"
+ ></i>
+ <i
+ role="button"
+ :aria-label="__('Clear search input')"
+ class="fa fa-times dropdown-input-clear"
+ :class="{
+ show: showClearInputButton
+ }"
+ @click="clearSearchInput"
+ ></i>
+ </div>
+ <div>
+ <virtual-list
+ :size="listHeight"
+ :remain="listShowCount"
+ wtag="ul"
+ ref="virtualScrollList"
+ >
+ <template v-if="filteredBlobsLength">
+ <li
+ v-for="(file, index) in filteredBlobs"
+ :key="file.key"
+ >
+ <item
+ class="disable-hover"
+ :file="file"
+ :search-text="searchText"
+ :focused="index === focusedIndex"
+ :index="index"
+ @click="openFile"
+ @mouseover="onMouseOver"
+ @mousemove="onMouseMove"
+ />
+ </li>
+ </template>
+ <li
+ v-else
+ class="dropdown-menu-empty-item"
+ >
+ <div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
+ <template v-if="loading">
+ {{ __('Loading...') }}
+ </template>
+ <template v-else>
+ {{ __('No files found.') }}
+ </template>
+ </div>
+ </li>
+ </virtual-list>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
new file mode 100644
index 00000000000..d4427420207
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/item.vue
@@ -0,0 +1,113 @@
+<script>
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import FileIcon from '../../../vue_shared/components/file_icon.vue';
+import ChangedFileIcon from '../changed_file_icon.vue';
+
+const MAX_PATH_LENGTH = 60;
+
+export default {
+ components: {
+ ChangedFileIcon,
+ FileIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ focused: {
+ type: Boolean,
+ required: true,
+ },
+ searchText: {
+ type: String,
+ required: true,
+ },
+ index: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ pathWithEllipsis() {
+ const path = this.file.path;
+
+ return path.length < MAX_PATH_LENGTH
+ ? path
+ : `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
+ },
+ nameSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.file.name, this.searchText);
+ },
+ pathSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
+ },
+ },
+ methods: {
+ clickRow() {
+ this.$emit('click', this.file);
+ },
+ mouseOverRow() {
+ this.$emit('mouseover', this.index);
+ },
+ mouseMove() {
+ this.$emit('mousemove', this.index);
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ class="diff-changed-file"
+ :class="{
+ 'is-focused': focused,
+ }"
+ @click.prevent="clickRow"
+ @mouseover="mouseOverRow"
+ @mousemove="mouseMove"
+ >
+ <file-icon
+ :file-name="file.name"
+ :size="16"
+ css-classes="diff-file-changed-icon append-right-8"
+ />
+ <span class="diff-changed-file-content append-right-8">
+ <strong
+ class="diff-changed-file-name"
+ >
+ <span
+ v-for="(char, index) in file.name.split('')"
+ :key="index + char"
+ :class="{
+ highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </strong>
+ <span
+ class="diff-changed-file-path prepend-top-5"
+ >
+ <span
+ v-for="(char, index) in pathWithEllipsis.split('')"
+ :key="index + char"
+ :class="{
+ highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </span>
+ </span>
+ <span
+ v-if="file.changed || file.tempFile"
+ class="diff-changed-stats"
+ >
+ <changed-file-icon
+ :file="file"
+ />
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 1c237c0ec97..2c184ea726c 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,35 +1,31 @@
<script>
-import { mapState, mapGetters } from 'vuex';
-import ideSidebar from './ide_side_bar.vue';
-import ideContextbar from './ide_context_bar.vue';
-import repoTabs from './repo_tabs.vue';
-import ideStatusBar from './ide_status_bar.vue';
-import repoEditor from './repo_editor.vue';
+import Mousetrap from 'mousetrap';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import IdeSidebar from './ide_side_bar.vue';
+import RepoTabs from './repo_tabs.vue';
+import IdeStatusBar from './ide_status_bar.vue';
+import RepoEditor from './repo_editor.vue';
+import FindFile from './file_finder/index.vue';
+
+const originalStopCallback = Mousetrap.stopCallback;
export default {
components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- ideStatusBar,
- repoEditor,
- },
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
+ IdeSidebar,
+ RepoTabs,
+ IdeStatusBar,
+ RepoEditor,
+ FindFile,
},
computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
+ ...mapState([
+ 'changedFiles',
+ 'openFiles',
+ 'viewer',
+ 'currentMergeRequestId',
+ 'fileFindVisible',
+ 'emptyStateSvgPath',
+ ]),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
@@ -42,67 +38,91 @@ export default {
});
return returnValue;
};
+
+ Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
+ this.toggleFileFinder(!this.fileFindVisible);
+ });
+
+ Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ mousetrapStopCallback(e, el, combo) {
+ if (
+ (combo === 't' && el.classList.contains('dropdown-input-field')) ||
+ el.classList.contains('inputarea')
+ ) {
+ return true;
+ } else if (combo === 'command+p' || combo === 'ctrl+p') {
+ return false;
+ }
+
+ return originalStopCallback(e, el, combo);
+ },
},
};
</script>
<template>
- <div
- class="ide-view"
- >
- <ide-sidebar />
+ <article class="ide">
<div
- class="multi-file-edit-pane"
+ class="ide-view"
>
- <template
- v-if="activeFile"
+ <find-file
+ v-show="fileFindVisible"
+ />
+ <ide-sidebar />
+ <div
+ class="multi-file-edit-pane"
>
- <repo-tabs
- :active-file="activeFile"
- :files="openFiles"
- :viewer="viewer"
- :has-changes="hasChanges"
- :merge-request-id="currentMergeRequestId"
- />
- <repo-editor
- class="multi-file-edit-pane-content"
- :file="activeFile"
- />
- <ide-status-bar
- :file="activeFile"
- />
- </template>
- <template
- v-else
- >
- <div
- v-once
- class="ide-empty-state"
+ <template
+ v-if="activeFile"
+ >
+ <repo-tabs
+ :active-file="activeFile"
+ :files="openFiles"
+ :viewer="viewer"
+ :has-changes="hasChanges"
+ :merge-request-id="currentMergeRequestId"
+ />
+ <repo-editor
+ class="multi-file-edit-pane-content"
+ :file="activeFile"
+ />
+ </template>
+ <template
+ v-else
>
- <div class="row js-empty-state">
- <div class="col-xs-12">
- <div class="svg-content svg-250">
- <img :src="emptyStateSvgPath" />
+ <div
+ v-once
+ class="ide-empty-state"
+ >
+ <div class="row js-empty-state">
+ <div class="col-12">
+ <div class="svg-content svg-250">
+ <img :src="emptyStateSvgPath" />
+ </div>
</div>
- </div>
- <div class="col-xs-12">
- <div class="text-content text-center">
- <h4>
- Welcome to the GitLab IDE
- </h4>
- <p>
- You can select a file in the left sidebar to begin
- editing and use the right sidebar to commit your changes.
- </p>
+ <div class="col-12">
+ <div class="text-content text-center">
+ <h4>
+ Welcome to the GitLab IDE
+ </h4>
+ <p>
+ Select a file from the left sidebar to begin editing.
+ Afterwards, you'll be able to commit your changes.
+ </p>
+ </div>
</div>
</div>
</div>
- </div>
- </template>
+ </template>
+ </div>
</div>
- <ide-contextbar
- :no-changes-state-svg-path="noChangesStateSvgPath"
- :committed-state-svg-path="committedStateSvgPath"
- />
- </div>
+ <ide-status-bar :file="activeFile"/>
+ </article>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
deleted file mode 100644
index 79a83b47994..00000000000
--- a/app/assets/javascripts/ide/components/ide_context_bar.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
-import icon from '~/vue_shared/components/icon.vue';
-import panelResizer from '~/vue_shared/components/panel_resizer.vue';
-import repoCommitSection from './repo_commit_section.vue';
-import ResizablePanel from './resizable_panel.vue';
-
-export default {
- components: {
- repoCommitSection,
- icon,
- panelResizer,
- ResizablePanel,
- },
- props: {
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState(['changedFiles', 'rightPanelCollapsed']),
- ...mapGetters(['currentIcon']),
- },
- methods: {
- ...mapActions(['setPanelCollapsedStatus']),
- },
-};
-</script>
-
-<template>
- <resizable-panel
- :collapsible="true"
- :initial-width="340"
- side="right"
- >
- <div
- class="multi-file-commit-panel-section"
- >
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
- >
- <div
- class="multi-file-commit-panel-header-title"
- v-if="!rightPanelCollapsed"
- >
- <div
- v-if="changedFiles.length"
- >
- <icon
- name="list-bulleted"
- :size="18"
- />
- Staged
- </div>
- </div>
- <button
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- @click.stop="setPanelCollapsedStatus({
- side: 'right',
- collapsed: !rightPanelCollapsed,
- })"
- >
- <icon
- :name="currentIcon"
- :size="18"
- />
- </button>
- </header>
- <repo-commit-section
- :no-changes-state-svg-path="noChangesStateSvgPath"
- :committed-state-svg-path="committedStateSvgPath"
- />
- </div>
- </resizable-panel>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_external_links.vue b/app/assets/javascripts/ide/components/ide_external_links.vue
deleted file mode 100644
index c6f6e0d2348..00000000000
--- a/app/assets/javascripts/ide/components/ide_external_links.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import icon from '~/vue_shared/components/icon.vue';
-
-export default {
- components: {
- icon,
- },
- props: {
- projectUrl: {
- type: String,
- required: true,
- },
- },
- computed: {
- goBackUrl() {
- return document.referrer || this.projectUrl;
- },
- },
-};
-</script>
-
-<template>
- <nav
- class="ide-external-links"
- v-once
- >
- <p>
- <a
- :href="goBackUrl"
- class="ide-sidebar-link"
- >
- <icon
- :size="16"
- class="append-right-8"
- name="go-back"
- />
- <span class="ide-external-links-text">
- {{ s__('Go back') }}
- </span>
- </a>
- </p>
- </nav>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue
index a6c6f46a144..30b00abf6ed 100644
--- a/app/assets/javascripts/ide/components/ide_file_buttons.vue
+++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue
@@ -32,14 +32,14 @@ export default {
<template>
<div
v-if="showButtons"
- class="pull-right ide-btn-group"
+ class="float-right ide-btn-group"
>
<a
v-tooltip
v-if="!file.binary"
:href="file.blamePath"
:title="__('Blame')"
- class="btn btn-xs btn-transparent blame"
+ class="btn btn-sm btn-transparent blame"
>
<icon
name="blame"
@@ -50,7 +50,7 @@ export default {
v-tooltip
:href="file.commitsPath"
:title="__('History')"
- class="btn btn-xs btn-transparent history"
+ class="btn btn-sm btn-transparent history"
>
<icon
name="history"
@@ -61,7 +61,7 @@ export default {
v-tooltip
:href="file.permalink"
:title="__('Permalink')"
- class="btn btn-xs btn-transparent permalink"
+ class="btn btn-sm btn-transparent permalink"
>
<icon
name="link"
@@ -72,7 +72,7 @@ export default {
v-tooltip
:href="file.rawPath"
target="_blank"
- class="btn btn-xs btn-transparent prepend-left-10 raw"
+ class="btn btn-sm btn-transparent prepend-left-10 raw"
rel="noopener noreferrer"
:title="rawDownloadButtonLabel">
<icon
diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
deleted file mode 100644
index eb2749e6151..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
- import icon from '~/vue_shared/components/icon.vue';
- import repoTree from './ide_repo_tree.vue';
- import newDropdown from './new_dropdown/index.vue';
-
- export default {
- components: {
- repoTree,
- icon,
- newDropdown,
- },
- props: {
- projectId: {
- type: String,
- required: true,
- },
- branch: {
- type: Object,
- required: true,
- },
- },
- };
-</script>
-
-<template>
- <div class="branch-container">
- <div class="branch-header">
- <div class="branch-header-title str-truncated ref-name">
- <icon
- name="branch"
- :size="12"
- />
- {{ branch.name }}
- </div>
- <div class="branch-header-btns">
- <new-dropdown
- :project-id="projectId"
- :branch="branch.name"
- path=""
- />
- </div>
- </div>
- <repo-tree
- :tree="branch.tree"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue
deleted file mode 100644
index a6f40286ac1..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_tree.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
-import Identicon from '../../vue_shared/components/identicon.vue';
-import BranchesTree from './ide_project_branches_tree.vue';
-import ExternalLinks from './ide_external_links.vue';
-
-export default {
- components: {
- BranchesTree,
- ExternalLinks,
- ProjectAvatarImage,
- Identicon,
- },
- props: {
- project: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="projects-sidebar">
- <div class="context-header">
- <a
- :title="project.name"
- :href="project.web_url"
- >
- <div
- v-if="project.avatar_url"
- class="avatar-container s40 project-avatar"
- >
- <project-avatar-image
- class="avatar-container project-avatar"
- :link-href="project.path"
- :img-src="project.avatar_url"
- :img-alt="project.name"
- :img-size="40"
- />
- </div>
- <identicon
- v-else
- size-class="s40"
- :entity-id="project.id"
- :entity-name="project.name"
- />
- <div class="sidebar-context-title">
- {{ project.name }}
- </div>
- </a>
- </div>
- <external-links
- :project-url="project.web_url"
- />
- <div class="multi-file-commit-panel-inner-scroll">
- <branches-tree
- v-for="branch in project.branches"
- :key="branch.name"
- :project-id="project.path_with_namespace"
- :branch="branch"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue
deleted file mode 100644
index e6af88e04bc..00000000000
--- a/app/assets/javascripts/ide/components/ide_repo_tree.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import RepoFile from './repo_file.vue';
-
-export default {
- components: {
- RepoFile,
- SkeletonLoadingContainer,
- },
- props: {
- tree: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div
- class="ide-file-list"
- >
- <template v-if="tree.loading">
- <div
- class="multi-file-loading-container"
- v-for="n in 3"
- :key="n"
- >
- <skeleton-loading-container />
- </div>
- </template>
- <template v-else>
- <repo-file
- v-for="file in tree.tree"
- :key="file.key"
- :file="file"
- :level="0"
- />
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
new file mode 100644
index 00000000000..0c9ec3b00f0
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -0,0 +1,62 @@
+<script>
+import { mapGetters, mapState, mapActions } from 'vuex';
+import IdeTreeList from './ide_tree_list.vue';
+import EditorModeDropdown from './editor_mode_dropdown.vue';
+import { viewerTypes } from '../constants';
+
+export default {
+ components: {
+ IdeTreeList,
+ EditorModeDropdown,
+ },
+ computed: {
+ ...mapGetters(['currentMergeRequest']),
+ ...mapState(['viewer']),
+ showLatestChangesText() {
+ return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
+ },
+ showMergeRequestText() {
+ return this.currentMergeRequest && this.viewer === viewerTypes.mr;
+ },
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
+ });
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <ide-tree-list
+ :viewer-type="viewer"
+ header-class="ide-review-header"
+ :disable-action-dropdown="true"
+ >
+ <template
+ slot="header"
+ >
+ <div class="ide-review-button-holder">
+ {{ __('Review') }}
+ <editor-mode-dropdown
+ v-if="currentMergeRequest"
+ :viewer="viewer"
+ :merge-request-id="currentMergeRequest.iid"
+ @click="updateViewer"
+ />
+ </div>
+ <div class="prepend-top-5 ide-review-sub-header">
+ <template v-if="showLatestChangesText">
+ {{ __('Latest changes') }}
+ </template>
+ <template v-else-if="showMergeRequestText">
+ {{ __('Merge request') }}
+ (<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
+ </template>
+ </div>
+ </template>
+ </ide-tree-list>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 8cf1ccb4fce..3f980203911 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,36 +1,82 @@
<script>
- import { mapState, mapGetters } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import panelResizer from '~/vue_shared/components/panel_resizer.vue';
- import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
- import projectTree from './ide_project_tree.vue';
- import ResizablePanel from './resizable_panel.vue';
+import { mapState, mapGetters } from 'vuex';
+import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import Identicon from '../../vue_shared/components/identicon.vue';
+import IdeTree from './ide_tree.vue';
+import ResizablePanel from './resizable_panel.vue';
+import ActivityBar from './activity_bar.vue';
+import CommitSection from './repo_commit_section.vue';
+import CommitForm from './commit_sidebar/form.vue';
+import IdeReview from './ide_review.vue';
+import SuccessMessage from './commit_sidebar/success_message.vue';
+import { activityBarViews } from '../constants';
- export default {
- components: {
- projectTree,
- icon,
- panelResizer,
- skeletonLoadingContainer,
- ResizablePanel,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ PanelResizer,
+ SkeletonLoadingContainer,
+ ResizablePanel,
+ ActivityBar,
+ ProjectAvatarImage,
+ Identicon,
+ CommitSection,
+ IdeTree,
+ CommitForm,
+ IdeReview,
+ SuccessMessage,
+ },
+ data() {
+ return {
+ showTooltip: false,
+ };
+ },
+ computed: {
+ ...mapState([
+ 'loading',
+ 'currentBranchId',
+ 'currentActivityView',
+ 'changedFiles',
+ 'stagedFiles',
+ 'lastCommitMsg',
+ ]),
+ ...mapGetters(['currentProject', 'someUncommitedChanges']),
+ showSuccessMessage() {
+ return (
+ this.currentActivityView === activityBarViews.edit &&
+ (this.lastCommitMsg && !this.someUncommitedChanges)
+ );
},
- computed: {
- ...mapState([
- 'loading',
- ]),
- ...mapGetters([
- 'projectsWithTrees',
- ]),
+ branchTooltipTitle() {
+ return this.showTooltip ? this.currentBranchId : undefined;
},
- };
+ },
+ watch: {
+ currentBranchId() {
+ this.$nextTick(() => {
+ this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
+ });
+ },
+ },
+};
</script>
<template>
<resizable-panel
:collapsible="false"
- :initial-width="290"
+ :initial-width="340"
side="left"
>
+ <activity-bar
+ v-if="!loading"
+ />
<div class="multi-file-commit-panel-inner">
<template v-if="loading">
<div
@@ -41,11 +87,54 @@
<skeleton-loading-container />
</div>
</template>
- <project-tree
- v-for="project in projectsWithTrees"
- :key="project.id"
- :project="project"
- />
+ <template v-else>
+ <div class="context-header ide-context-header">
+ <a
+ :href="currentProject.web_url"
+ >
+ <div
+ v-if="currentProject.avatar_url"
+ class="avatar-container s40 project-avatar"
+ >
+ <project-avatar-image
+ class="avatar-container project-avatar"
+ :link-href="currentProject.path"
+ :img-src="currentProject.avatar_url"
+ :img-alt="currentProject.name"
+ :img-size="40"
+ />
+ </div>
+ <identicon
+ v-else
+ size-class="s40"
+ :entity-id="currentProject.id"
+ :entity-name="currentProject.name"
+ />
+ <div class="ide-sidebar-project-title">
+ <div class="sidebar-context-title">
+ {{ currentProject.name }}
+ </div>
+ <div
+ class="sidebar-context-title ide-sidebar-branch-title"
+ ref="branchId"
+ v-tooltip
+ :title="branchTooltipTitle"
+ >
+ <icon
+ name="branch"
+ css-classes="append-right-5"
+ />{{ currentBranchId }}
+ </div>
+ </div>
+ </a>
+ </div>
+ <div class="multi-file-commit-panel-inner-scroll">
+ <component
+ :is="currentActivityView"
+ />
+ </div>
+ <commit-form />
+ </template>
</div>
</resizable-panel>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 152a5f632ad..6f60cfbf184 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,11 +1,16 @@
<script>
+import { mapActions, mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
+import CiIcon from '../../vue_shared/components/ci_icon.vue';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
components: {
icon,
+ userAvatarImage,
+ CiIcon,
},
directives: {
tooltip,
@@ -14,47 +19,126 @@ export default {
props: {
file: {
type: Object,
- required: true,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ lastCommitFormatedAge: null,
+ };
+ },
+ computed: {
+ ...mapState(['currentBranchId', 'currentProjectId']),
+ ...mapGetters(['currentProject', 'lastCommit']),
+ },
+ watch: {
+ lastCommit() {
+ if (!this.isPollingInitialized) {
+ this.initPipelinePolling();
+ }
+ },
+ },
+ mounted() {
+ this.startTimer();
+ },
+ beforeDestroy() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ }
+ if (this.isPollingInitialized) {
+ this.stopPipelinePolling();
+ }
+ },
+ methods: {
+ ...mapActions(['pipelinePoll', 'stopPipelinePolling']),
+ startTimer() {
+ this.intervalId = setInterval(() => {
+ this.commitAgeUpdate();
+ }, 1000);
+ },
+ initPipelinePolling() {
+ this.pipelinePoll();
+ this.isPollingInitialized = true;
+ },
+ commitAgeUpdate() {
+ if (this.lastCommit) {
+ this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
+ }
+ },
+ getCommitPath(shortSha) {
+ return `${this.currentProject.web_url}/commit/${shortSha}`;
},
},
};
</script>
<template>
- <div class="ide-status-bar">
- <div class="ref-name">
+ <footer class="ide-status-bar">
+ <div
+ class="ide-status-branch"
+ v-if="lastCommit && lastCommitFormatedAge"
+ >
+ <span
+ class="ide-status-pipeline"
+ v-if="lastCommit.pipeline && lastCommit.pipeline.details"
+ >
+ <ci-icon
+ :status="lastCommit.pipeline.details.status"
+ v-tooltip
+ :title="lastCommit.pipeline.details.status.text"
+ />
+ Pipeline
+ <a
+ class="monospace"
+ :href="lastCommit.pipeline.details.status.details_path">#{{ lastCommit.pipeline.id }}</a>
+ {{ lastCommit.pipeline.details.status.text }}
+ for
+ </span>
+
<icon
- name="branch"
- :size="12"
+ name="commit"
/>
- {{ file.branchId }}
- </div>
- <div>
- <div v-if="file.lastCommit && file.lastCommit.id">
- Last commit:
- <a
- v-tooltip
- :title="file.lastCommit.message"
- :href="file.lastCommit.url"
- >
- {{ timeFormated(file.lastCommit.updatedAt) }} by
- {{ file.lastCommit.author }}
- </a>
- </div>
+ <a
+ v-tooltip
+ class="commit-sha"
+ :title="lastCommit.message"
+ :href="getCommitPath(lastCommit.short_id)"
+ >{{ lastCommit.short_id }}</a>
+ by
+ {{ lastCommit.author_name }}
+ <time
+ v-tooltip
+ data-placement="top"
+ data-container="body"
+ :datetime="lastCommit.committed_date"
+ :title="tooltipTitle(lastCommit.committed_date)"
+ >
+ {{ lastCommitFormatedAge }}
+ </time>
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.name }}
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.eol }}
</div>
<div
- class="text-right"
- v-if="!file.binary">
+ class="ide-status-file"
+ v-if="file && !file.binary">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.fileLanguage }}
</div>
- </div>
+ </footer>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
new file mode 100644
index 00000000000..8fc4ebe6ca6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -0,0 +1,42 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import NewDropdown from './new_dropdown/index.vue';
+import IdeTreeList from './ide_tree_list.vue';
+
+export default {
+ components: {
+ NewDropdown,
+ IdeTreeList,
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ ...mapGetters(['currentProject', 'currentTree', 'activeFile']),
+ },
+ mounted() {
+ if (this.activeFile && this.activeFile.pending) {
+ this.$router.push(`/project${this.activeFile.url}`, () => {
+ this.updateViewer('editor');
+ });
+ }
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <ide-tree-list
+ viewer-type="editor"
+ >
+ <template
+ slot="header"
+ >
+ {{ __('Edit') }}
+ <new-dropdown
+ :project-id="currentProject.name_with_namespace"
+ :branch="currentBranchId"
+ />
+ </template>
+ </ide-tree-list>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
new file mode 100644
index 00000000000..e64a09fcc90
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -0,0 +1,76 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import RepoFile from './repo_file.vue';
+import NewDropdown from './new_dropdown/index.vue';
+
+export default {
+ components: {
+ Icon,
+ RepoFile,
+ SkeletonLoadingContainer,
+ NewDropdown,
+ },
+ props: {
+ viewerType: {
+ type: String,
+ required: true,
+ },
+ headerClass: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ disableActionDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ ...mapGetters(['currentProject', 'currentTree']),
+ showLoading() {
+ return !this.currentTree || this.currentTree.loading;
+ },
+ },
+ mounted() {
+ this.updateViewer(this.viewerType);
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-list"
+ >
+ <template v-if="showLoading">
+ <div
+ class="multi-file-loading-container"
+ v-for="n in 3"
+ :key="n"
+ >
+ <skeleton-loading-container />
+ </div>
+ </template>
+ <template v-else>
+ <header
+ class="ide-tree-header"
+ :class="headerClass"
+ >
+ <slot name="header"></slot>
+ </header>
+ <repo-file
+ v-for="file in currentTree.tree"
+ :key="file.key"
+ :file="file"
+ :level="0"
+ :disable-action-dropdown="disableActionDropdown"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
index 8a440902dfc..179a589d1ac 100644
--- a/app/assets/javascripts/ide/components/mr_file_icon.vue
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -16,8 +16,8 @@ export default {
<icon
name="git-merge"
v-tooltip
- title="__('Part of merge request changes')"
- css-classes="ide-file-changed-icon"
+ :title="__('Part of merge request changes')"
+ css-classes="append-right-8"
:size="12"
/>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 769e9b79cad..f0b29702497 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -1,49 +1,55 @@
<script>
- import { mapActions } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import newModal from './modal.vue';
- import upload from './upload.vue';
+import { mapActions } from 'vuex';
+import icon from '~/vue_shared/components/icon.vue';
+import newModal from './modal.vue';
+import upload from './upload.vue';
- export default {
- components: {
- icon,
- newModal,
- upload,
+export default {
+ components: {
+ icon,
+ newModal,
+ upload,
+ },
+ props: {
+ branch: {
+ type: String,
+ required: true,
},
- props: {
- branch: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: true,
- },
+ path: {
+ type: String,
+ required: false,
+ default: '',
},
- data() {
- return {
- openModal: false,
- modalType: '',
- dropdownOpen: false,
- };
+ },
+ data() {
+ return {
+ openModal: false,
+ modalType: '',
+ dropdownOpen: false,
+ };
+ },
+ watch: {
+ dropdownOpen() {
+ this.$nextTick(() => {
+ this.$refs.dropdownMenu.scrollIntoView();
+ });
},
- methods: {
- ...mapActions([
- 'createTempEntry',
- ]),
- createNewItem(type) {
- this.modalType = type;
- this.openModal = true;
- this.dropdownOpen = false;
- },
- hideModal() {
- this.openModal = false;
- },
- openDropdown() {
- this.dropdownOpen = !this.dropdownOpen;
- },
+ },
+ methods: {
+ ...mapActions(['createTempEntry']),
+ createNewItem(type) {
+ this.modalType = type;
+ this.openModal = true;
+ this.dropdownOpen = false;
},
- };
+ hideModal() {
+ this.openModal = false;
+ },
+ openDropdown() {
+ this.dropdownOpen = !this.dropdownOpen;
+ },
+ },
+};
</script>
<template>
@@ -51,7 +57,7 @@
<div
class="dropdown"
:class="{
- open: dropdownOpen,
+ show: dropdownOpen,
}"
>
<button
@@ -63,15 +69,18 @@
<icon
name="plus"
:size="12"
- css-classes="pull-left"
+ css-classes="float-left"
/>
<icon
name="arrow-down"
:size="12"
- css-classes="pull-left"
+ css-classes="float-left"
/>
</button>
- <ul class="dropdown-menu dropdown-menu-right">
+ <ul
+ class="dropdown-menu dropdown-menu-right"
+ ref="dropdownMenu"
+ >
<li>
<a
href="#"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 4b5a50785b6..d83a90f71e1 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -40,13 +40,6 @@ export default {
return __('Create file');
},
- formLabelName() {
- if (this.type === 'tree') {
- return __('Directory name');
- }
-
- return __('File name');
- },
},
mounted() {
this.$refs.fieldName.focus();
@@ -77,13 +70,13 @@ export default {
@submit="createEntryInStore"
>
<form
- class="form-horizontal"
slot="body"
@submit.prevent="createEntryInStore"
+ class="form-group row append-bottom-0"
>
<fieldset class="form-group append-bottom-0">
- <label class="label-light col-sm-3">
- {{ formLabelName }}
+ <label class="label-light col-form-label col-sm-3 ide-new-modal-label">
+ {{ __('Name') }}
</label>
<div class="col-sm-9">
<input
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 14673754503..c5092d8e04d 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -1,64 +1,63 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
-import commitFilesList from './commit_sidebar/list.vue';
-import CommitMessageField from './commit_sidebar/message_field.vue';
+import CommitFilesList from './commit_sidebar/list.vue';
+import EmptyState from './commit_sidebar/empty_state.vue';
import * as consts from '../stores/modules/commit/constants';
-import Actions from './commit_sidebar/actions.vue';
+import { activityBarViews } from '../constants';
export default {
components: {
DeprecatedModal,
- icon,
- commitFilesList,
- Actions,
- LoadingButton,
- CommitMessageField,
+ Icon,
+ CommitFilesList,
+ EmptyState,
},
directives: {
tooltip,
},
- props: {
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
computed: {
...mapState([
- 'currentProjectId',
- 'currentBranchId',
+ 'changedFiles',
+ 'stagedFiles',
'rightPanelCollapsed',
'lastCommitMsg',
- 'changedFiles',
+ 'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
- statusSvg() {
- return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
+ ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ showStageUnstageArea() {
+ return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
},
- methods: {
- ...mapActions(['setPanelCollapsedStatus']),
- ...mapActions('commit', [
- 'updateCommitMessage',
- 'discardDraft',
- 'commitChanges',
- 'updateCommitAction',
- ]),
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'right',
- collapsed: !this.rightPanelCollapsed,
- });
+ watch: {
+ hasChanges() {
+ if (!this.hasChanges) {
+ this.updateActivityBarView(activityBarViews.edit);
+ }
},
+ },
+ mounted() {
+ if (this.lastOpenedFile) {
+ this.openPendingTab({
+ file: this.lastOpenedFile,
+ })
+ .then(changeViewer => {
+ if (changeViewer) {
+ this.updateViewer('diff');
+ }
+ })
+ .catch(e => {
+ throw e;
+ });
+ }
+ },
+ methods: {
+ ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
+ ...mapActions('commit', ['commitChanges', 'updateCommitAction']),
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
@@ -69,9 +68,6 @@ export default {
<template>
<div
class="multi-file-commit-panel-section"
- :class="{
- 'multi-file-commit-empty-state-container': !changedFiles.length
- }"
>
<deprecated-modal
id="ide-create-branch-modal"
@@ -85,76 +81,30 @@ export default {
Would you like to create a new branch?`) }}
</template>
</deprecated-modal>
- <commit-files-list
- title="Staged"
- :file-list="changedFiles"
- :collapsed="rightPanelCollapsed"
- @toggleCollapsed="toggleCollapsed"
- />
<template
- v-if="changedFiles.length"
+ v-if="showStageUnstageArea"
>
- <form
- class="form-horizontal multi-file-commit-form"
- @submit.prevent.stop="commitChanges"
- v-if="!rightPanelCollapsed"
- >
- <commit-message-field
- :text="commitMessage"
- @input="updateCommitMessage"
- />
- <div class="clearfix prepend-top-15">
- <actions />
- <loading-button
- :loading="submitCommitLoading"
- :disabled="commitButtonDisabled"
- container-class="btn btn-success btn-sm pull-left"
- :label="__('Commit')"
- @click="commitChanges"
- />
- <button
- v-if="!discardDraftButtonDisabled"
- type="button"
- class="btn btn-default btn-sm pull-right"
- @click="discardDraft"
- >
- {{ __('Discard draft') }}
- </button>
- </div>
- </form>
+ <commit-files-list
+ class="is-first"
+ icon-name="unstaged"
+ :title="__('Unstaged')"
+ :file-list="changedFiles"
+ action="stageAllChanges"
+ :action-btn-text="__('Stage all')"
+ item-action-component="stage-button"
+ />
+ <commit-files-list
+ icon-name="staged"
+ :title="__('Staged')"
+ :file-list="stagedFiles"
+ action="unstageAllChanges"
+ :action-btn-text="__('Unstage all')"
+ item-action-component="unstage-button"
+ :staged-list="true"
+ />
</template>
- <div
- v-else-if="!rightPanelCollapsed"
- class="row js-empty-state"
- >
- <div class="col-xs-10 col-xs-offset-1">
- <div class="svg-content svg-80">
- <img :src="statusSvg" />
- </div>
- </div>
- <div class="col-xs-10 col-xs-offset-1">
- <div
- class="text-content text-center"
- v-if="!lastCommitMsg"
- >
- <h4>
- {{ __('No changes') }}
- </h4>
- <p>
- {{ __('Edit files in the editor and commit changes here') }}
- </p>
- </div>
- <div
- class="text-content text-center"
- v-else
- >
- <h4>
- {{ __('All changes are committed') }}
- </h4>
- <p v-html="lastCommitMsg">
- </p>
- </div>
- </div>
- </div>
+ <empty-state
+ v-if="unusedSeal"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 711bafa17a9..a281ecb95c8 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -3,6 +3,7 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
+import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue';
@@ -19,8 +20,14 @@ export default {
},
},
computed: {
- ...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
- ...mapGetters(['currentMergeRequest']),
+ ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
+ ...mapGetters([
+ 'currentMergeRequest',
+ 'getStagedFile',
+ 'isEditModeActive',
+ 'isCommitModeActive',
+ 'isReviewModeActive',
+ ]),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
@@ -36,10 +43,29 @@ export default {
},
},
watch: {
- file(oldVal, newVal) {
+ file(newVal, oldVal) {
+ if (oldVal.pending) {
+ this.removePendingTab(oldVal);
+ }
+
// Compare key to allow for files opened in review mode to be cached differently
- if (newVal.key !== this.file.key) {
+ if (oldVal.key !== this.file.key) {
this.initMonaco();
+
+ if (this.currentActivityView !== activityBarViews.edit) {
+ this.setFileViewMode({
+ file: this.file,
+ viewMode: 'edit',
+ });
+ }
+ }
+ },
+ currentActivityView() {
+ if (this.currentActivityView !== activityBarViews.edit) {
+ this.setFileViewMode({
+ file: this.file,
+ viewMode: 'edit',
+ });
}
},
rightPanelCollapsed() {
@@ -77,7 +103,7 @@ export default {
'setFileViewMode',
'setFileEOL',
'updateViewer',
- 'updateDelayViewerUpdated',
+ 'removePendingTab',
]),
initMonaco() {
if (this.shouldHideEditor) return;
@@ -89,14 +115,6 @@ export default {
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => {
- const viewerPromise = this.delayViewerUpdated
- ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
- : Promise.resolve();
-
- return viewerPromise;
- })
- .then(() => {
- this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.catch(err => {
@@ -108,10 +126,10 @@ export default {
this.editor.dispose();
this.$nextTick(() => {
- if (this.viewer === 'editor') {
+ if (this.viewer === viewerTypes.edit) {
this.editor.createInstance(this.$refs.editor);
} else {
- this.editor.createDiffInstance(this.$refs.editor);
+ this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
}
this.setupEditor();
@@ -120,9 +138,14 @@ export default {
setupEditor() {
if (!this.file || !this.editor.instance) return;
- this.model = this.editor.createModel(this.file);
+ const head = this.getStagedFile(this.file.path);
+
+ this.model = this.editor.createModel(
+ this.file,
+ this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
+ );
- if (this.viewer === 'mrdiff') {
+ if (this.viewer === viewerTypes.mr && this.file.mrChange) {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
@@ -163,6 +186,7 @@ export default {
});
},
},
+ viewerTypes,
};
</script>
@@ -171,16 +195,17 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
- <div class="ide-mode-tabs clearfix">
+ <div class="ide-mode-tabs clearfix" >
<ul
- class="nav-links pull-left"
- v-if="!shouldHideEditor">
+ class="nav-links float-left"
+ v-if="!shouldHideEditor && isEditModeActive"
+ >
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
- <template v-if="viewer === 'editor'">
+ <template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
<template v-else>
@@ -207,6 +232,9 @@ export default {
v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
+ :class="{
+ 'is-readonly': isCommitModeActive,
+ }"
>
</div>
<content-viewer
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 3b5068d4910..442697e1c80 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -1,22 +1,29 @@
<script>
-import { mapActions } from 'vuex';
-import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import fileIcon from '~/vue_shared/components/file_icon.vue';
+import { mapActions, mapGetters } from 'vuex';
+import { n__, __, sprintf } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router';
-import newDropdown from './new_dropdown/index.vue';
-import fileStatusIcon from './repo_file_status_icon.vue';
-import changedFileIcon from './changed_file_icon.vue';
-import mrFileIcon from './mr_file_icon.vue';
+import NewDropdown from './new_dropdown/index.vue';
+import FileStatusIcon from './repo_file_status_icon.vue';
+import ChangedFileIcon from './changed_file_icon.vue';
+import MrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
+ directives: {
+ tooltip,
+ },
components: {
- skeletonLoadingContainer,
- newDropdown,
- fileStatusIcon,
- fileIcon,
- changedFileIcon,
- mrFileIcon,
+ SkeletonLoadingContainer,
+ NewDropdown,
+ FileStatusIcon,
+ FileIcon,
+ ChangedFileIcon,
+ MrFileIcon,
+ Icon,
},
props: {
file: {
@@ -27,8 +34,41 @@ export default {
type: Number,
required: true,
},
+ disableActionDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
+ ...mapGetters([
+ 'getChangesInFolder',
+ 'getUnstagedFilesCountForPath',
+ 'getStagedFilesCountForPath',
+ ]),
+ folderUnstagedCount() {
+ return this.getUnstagedFilesCountForPath(this.file.path);
+ },
+ folderStagedCount() {
+ return this.getStagedFilesCountForPath(this.file.path);
+ },
+ changesCount() {
+ return this.getChangesInFolder(this.file.path);
+ },
+ folderChangesTooltip() {
+ if (this.changesCount === 0) return undefined;
+
+ if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
+ return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
+ } else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
+ return n__('%d staged change', '%d staged changes', this.folderStagedCount);
+ }
+
+ return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
+ unstaged: this.folderUnstagedCount,
+ staged: this.folderStagedCount,
+ });
+ },
isTree() {
return this.file.type === 'tree';
},
@@ -48,23 +88,30 @@ export default {
'is-open': this.file.opened,
};
},
+ showTreeChangesCount() {
+ return this.isTree && this.changesCount > 0 && !this.file.opened;
+ },
+ showChangedFileIcon() {
+ return this.file.changed || this.file.tempFile || this.file.staged;
+ },
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
- this.$el.scrollIntoView();
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ });
}
},
methods: {
- ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
+ ...mapActions(['toggleTreeOpen']),
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
- return this.updateDelayViewerUpdated(true).then(() => {
- router.push(`/project${this.file.url}`);
- });
+ router.push(`/project${this.file.url}`);
},
},
};
@@ -75,11 +122,11 @@ export default {
<div
class="file"
:class="fileClass"
+ @click="clickFile"
+ role="button"
>
<div
class="file-name"
- @click="clickFile"
- role="button"
>
<span
class="ide-file-name str-truncated"
@@ -97,21 +144,40 @@ export default {
:file="file"
/>
</span>
- <span class="pull-right">
+ <span class="float-right ide-file-icon-holder">
<mr-file-icon
v-if="file.mrChange"
/>
+ <span
+ v-if="showTreeChangesCount"
+ class="ide-tree-changes"
+ >
+ {{ changesCount }}
+ <icon
+ v-tooltip
+ :title="folderChangesTooltip"
+ data-container="body"
+ data-placement="right"
+ name="file-modified"
+ :size="12"
+ css-classes="prepend-left-5 multi-file-modified"
+ />
+ </span>
<changed-file-icon
+ v-else-if="showChangedFileIcon"
:file="file"
- v-if="file.changed || file.tempFile"
+ :show-tooltip="true"
+ :show-staged-icon="true"
+ :force-modified-icon="true"
+ class="pull-right"
/>
</span>
<new-dropdown
- v-if="isTree"
+ v-if="isTree && !disableActionDropdown"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
- class="pull-right prepend-left-8"
+ class="float-right prepend-left-8"
/>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
index 79af8c0b0c7..3e47da88050 100644
--- a/app/assets/javascripts/ide/components/repo_loading_file.vue
+++ b/app/assets/javascripts/ide/components/repo_loading_file.vue
@@ -25,13 +25,13 @@
/>
</td>
<template v-if="!leftPanelCollapsed">
- <td class="hidden-sm hidden-xs">
+ <td class="d-none d-sm-none d-md-block">
<skeleton-loading-container
:small="true"
/>
</td>
- <td class="hidden-xs">
+ <td class="d-none d-sm-block">
<skeleton-loading-container
class="animation-container-right"
:small="true"
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 304a73ed1ad..fb26b973236 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -26,13 +26,18 @@ export default {
},
computed: {
closeLabel() {
- if (this.tab.changed || this.tab.tempFile) {
+ if (this.fileHasChanged) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
showChangedIcon() {
- return this.tab.changed ? !this.tabMouseOver : false;
+ if (this.tab.pending) return true;
+
+ return this.fileHasChanged ? !this.tabMouseOver : false;
+ },
+ fileHasChanged() {
+ return this.tab.changed || this.tab.tempFile || this.tab.staged;
},
},
@@ -42,18 +47,18 @@ export default {
this.updateDelayViewerUpdated(true);
if (tab.pending) {
- this.openPendingTab(tab);
+ this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
} else {
this.$router.push(`/project${tab.url}`);
}
},
mouseOverTab() {
- if (this.tab.changed) {
+ if (this.fileHasChanged) {
this.tabMouseOver = true;
}
},
mouseOutTab() {
- if (this.tab.changed) {
+ if (this.fileHasChanged) {
this.tabMouseOver = false;
}
},
@@ -63,15 +68,32 @@ export default {
<template>
<li
+ :class="{
+ active: tab.active
+ }"
@click="clickFile(tab)"
@mouseover="mouseOverTab"
@mouseout="mouseOutTab"
>
+ <div
+ class="multi-file-tab"
+ :title="tab.url"
+ >
+ <file-icon
+ :file-name="tab.name"
+ :size="16"
+ />
+ {{ tab.name }}
+ <file-status-icon
+ :file="tab"
+ />
+ </div>
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"
+ :disabled="tab.pending"
>
<icon
v-if="!showChangedIcon"
@@ -81,24 +103,8 @@ export default {
<changed-file-icon
v-else
:file="tab"
+ :force-modified-icon="true"
/>
</button>
-
- <div
- class="multi-file-tab"
- :class="{
- active: tab.active
- }"
- :title="tab.url"
- >
- <file-icon
- :file-name="tab.name"
- :size="16"
- />
- {{ tab.name }}
- <file-status-icon
- :file="tab"
- />
- </div>
</li>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 7bd646ba9b0..99e51097e12 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -32,16 +32,6 @@ export default {
default: '',
},
},
- data() {
- return {
- showShadow: false,
- };
- },
- updated() {
- if (!this.$refs.tabsScroller) return;
-
- this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
- },
methods: {
...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) {
@@ -71,12 +61,5 @@ export default {
:tab="tab"
/>
</ul>
- <editor-mode
- :viewer="viewer"
- :show-shadow="showShadow"
- :has-changes="hasChanges"
- :merge-request-id="mergeRequestId"
- @click="openFileViewer"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index b60d042e0be..83fe22f40a4 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -1,3 +1,22 @@
// Fuzzy file finder
+export const MAX_FILE_FINDER_RESULTS = 40;
+export const FILE_FINDER_ROW_HEIGHT = 55;
+export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
+
+export const MAX_WINDOW_HEIGHT_COMPACT = 750;
+
+// Commit message textarea
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
+
+export const activityBarViews = {
+ edit: 'ide-tree',
+ commit: 'commit-section',
+ review: 'ide-review',
+};
+
+export const viewerTypes = {
+ mr: 'mrdiff',
+ edit: 'editor',
+ diff: 'diff',
+};
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 4a0a303d5a6..a21cec4e8d8 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import flash from '~/flash';
import store from './stores';
+import { activityBarViews } from './constants';
Vue.use(VueRouter);
@@ -40,7 +41,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
children: [
{
- path: ':targetmode(edit|tree|blob)/:branch/*',
+ path: ':targetmode(edit|tree|blob)/*',
component: EmptyRouterComponent,
},
{
@@ -62,21 +63,27 @@ router.beforeEach((to, from, next) => {
.then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
- if (to.params.branch) {
+ const baseSplit = to.params[0].split('/-/');
+ const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0];
+
+ if (branchId) {
+ const basePath = baseSplit.length > 1 ? baseSplit[1] : '';
+
+ store.dispatch('setCurrentBranchId', branchId);
+
store.dispatch('getBranchData', {
projectId: fullProjectId,
- branchId: to.params.branch,
+ branchId,
});
store
.dispatch('getFiles', {
projectId: fullProjectId,
- branchId: to.params.branch,
+ branchId,
})
.then(() => {
- if (to.params[0]) {
- const path =
- to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
+ if (basePath) {
+ const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(store.state.entries).find(
key => key === path && !store.state.entries[key].pending,
);
@@ -99,14 +106,14 @@ router.beforeEach((to, from, next) => {
throw e;
});
} else if (to.params.mrid) {
- store.dispatch('updateViewer', 'mrdiff');
-
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mr => {
+ store.dispatch('updateActivityBarView', activityBarViews.review);
+
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index cbfb3dc54f2..c5835cd3b06 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -4,7 +4,9 @@ import ide from './components/ide.vue';
import store from './stores';
import router from './ide_router';
-function initIde(el) {
+Vue.use(Translate);
+
+export function initIde(el) {
if (!el) return null;
return new Vue({
@@ -14,20 +16,25 @@ function initIde(el) {
components: {
ide,
},
- render(createElement) {
- return createElement('ide', {
- props: {
- emptyStateSvgPath: el.dataset.emptyStateSvgPath,
- noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
- committedStateSvgPath: el.dataset.committedStateSvgPath,
- },
+ created() {
+ this.$store.dispatch('setEmptyStateSvgs', {
+ emptyStateSvgPath: el.dataset.emptyStateSvgPath,
+ noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
+ committedStateSvgPath: el.dataset.committedStateSvgPath,
});
},
+ render(createElement) {
+ return createElement('ide');
+ },
});
}
-const ideElement = document.getElementById('ide');
-
-Vue.use(Translate);
-
-initIde(ideElement);
+// tell webpack to load assets from origin so that web workers don't break
+export function resetServiceWorkersPublicPath() {
+ // __webpack_public_path__ is a global variable that can be used to adjust
+ // the webpack publicPath setting at runtime.
+ // see: https://webpack.js.org/guides/public-path/
+ const relativeRootPath = (gon && gon.relative_url_root) || '';
+ const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
+ __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
+}
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index e47adae99ed..b1e43a1e38c 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -3,22 +3,23 @@ import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
- constructor(monaco, file) {
+ constructor(monaco, file, head = null) {
this.monaco = monaco;
this.disposable = new Disposable();
this.file = file;
+ this.head = head;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
(this.originalModel = this.monaco.editor.createModel(
- this.file.raw,
+ head ? head.content : this.file.raw,
undefined,
- new this.monaco.Uri(null, null, `original/${this.file.key}`),
+ new this.monaco.Uri(null, null, `original/${this.path}`),
)),
(this.model = this.monaco.editor.createModel(
this.content,
undefined,
- new this.monaco.Uri(null, null, this.file.key),
+ new this.monaco.Uri(null, null, this.path),
)),
);
if (this.file.mrChange) {
@@ -26,18 +27,20 @@ export default class Model {
(this.baseModel = this.monaco.editor.createModel(
this.file.baseRaw,
undefined,
- new this.monaco.Uri(null, null, `target/${this.file.path}`),
+ new this.monaco.Uri(null, null, `target/${this.path}`),
)),
);
}
- this.events = new Map();
+ this.events = new Set();
this.updateContent = this.updateContent.bind(this);
+ this.updateNewContent = this.updateNewContent.bind(this);
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
- eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
+ eventHub.$on(`editor.update.model.content.${this.file.key}`, this.updateContent);
+ eventHub.$on(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
get url() {
@@ -73,22 +76,36 @@ export default class Model {
}
onChange(cb) {
- this.events.set(
- this.path,
- this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
- );
+ this.events.add(this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))));
+ }
+
+ onDispose(cb) {
+ this.events.add(cb);
}
- updateContent(content) {
+ updateContent({ content, changed }) {
this.getOriginalModel().setValue(content);
+
+ if (!changed) {
+ this.getModel().setValue(content);
+ }
+ }
+
+ updateNewContent(content) {
this.getModel().setValue(content);
}
dispose() {
this.disposable.dispose();
+
+ this.events.forEach(cb => {
+ if (typeof cb === 'function') cb();
+ });
+
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
- eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
+ eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent);
+ eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 0e7b563b5d6..7f643969480 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -17,12 +17,12 @@ export default class ModelManager {
return this.models.get(key);
}
- addModel(file) {
+ addModel(file, head = null) {
if (this.hasCachedModel(file.key)) {
return this.getModel(file.key);
}
- const model = new Model(this.monaco, file);
+ const model = new Model(this.monaco, file, head);
this.models.set(model.path, model);
this.disposable.add(model);
diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
index 42904774747..13d477bb2cf 100644
--- a/app/assets/javascripts/ide/lib/decorations/controller.js
+++ b/app/assets/javascripts/ide/lib/decorations/controller.js
@@ -38,6 +38,15 @@ export default class DecorationsController {
);
}
+ hasDecorations(model) {
+ return this.decorations.has(model.url);
+ }
+
+ removeDecorations(model) {
+ this.decorations.delete(model.url);
+ this.editorDecorations.delete(model.url);
+ }
+
dispose() {
this.decorations.clear();
this.editorDecorations.clear();
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index b136545ad11..f579424cf33 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -3,7 +3,7 @@ import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
-export const getDiffChangeType = (change) => {
+export const getDiffChangeType = change => {
if (change.modified) {
return 'modified';
} else if (change.added) {
@@ -16,12 +16,7 @@ export const getDiffChangeType = (change) => {
};
export const getDecorator = change => ({
- range: new monaco.Range(
- change.lineNumber,
- 1,
- change.endLineNumber,
- 1,
- ),
+ range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
@@ -31,6 +26,7 @@ export const getDecorator = change => ({
export default class DirtyDiffController {
constructor(modelManager, decorationsController) {
this.disposable = new Disposable();
+ this.models = new Map();
this.editorSimpleWorker = null;
this.modelManager = modelManager;
this.decorationsController = decorationsController;
@@ -42,7 +38,15 @@ export default class DirtyDiffController {
}
attachModel(model) {
+ if (this.models.has(model.url)) return;
+
model.onChange(() => this.throttledComputeDiff(model));
+ model.onDispose(() => {
+ this.decorationsController.removeDecorations(model);
+ this.models.delete(model.url);
+ });
+
+ this.models.set(model.url, model);
}
computeDiff(model) {
@@ -54,7 +58,11 @@ export default class DirtyDiffController {
}
reDecorate(model) {
- this.decorationsController.decorate(model);
+ if (this.decorationsController.hasDecorations(model)) {
+ this.decorationsController.decorate(model);
+ } else {
+ this.computeDiff(model);
+ }
}
decorate({ data }) {
@@ -65,6 +73,7 @@ export default class DirtyDiffController {
dispose() {
this.disposable.dispose();
+ this.models.clear();
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate();
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 001737d6ee8..9c3bb9cc17d 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,10 +1,12 @@
import _ from 'underscore';
+import store from '../stores';
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
+import keymap from './keymap.json';
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
@@ -53,32 +55,36 @@ export default class Editor {
)),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
- createDiffInstance(domElement) {
+ createDiffInstance(domElement, readOnly = true) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
- readOnly: true,
quickSuggestions: false,
occurrencesHighlight: false,
- renderLineHighlight: 'none',
- hideCursorInOverviewRuler: true,
renderSideBySide: Editor.renderSideBySide(domElement),
+ readOnly,
+ renderLineHighlight: readOnly ? 'all' : 'none',
+ hideCursorInOverviewRuler: !readOnly,
})),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
- createModel(file) {
- return this.modelManager.addModel(file);
+ createModel(file, head = null) {
+ return this.modelManager.addModel(file, head);
}
attachModel(model) {
@@ -189,4 +195,31 @@ export default class Editor {
static renderSideBySide(domElement) {
return domElement.offsetWidth >= 700;
}
+
+ addCommands() {
+ const getKeyCode = key => {
+ const monacoKeyMod = key.indexOf('KEY_') === 0;
+
+ return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
+ };
+
+ keymap.forEach(command => {
+ const keybindings = command.bindings.map(binding => {
+ const keys = binding.split('+');
+
+ // eslint-disable-next-line no-bitwise
+ return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
+ });
+
+ this.instance.addAction({
+ id: command.id,
+ label: command.label,
+ keybindings,
+ run() {
+ store.dispatch(command.action.name, command.action.params);
+ return null;
+ },
+ });
+ });
+ }
}
diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json
new file mode 100644
index 00000000000..131abfebbed
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/keymap.json
@@ -0,0 +1,11 @@
+[
+ {
+ "id": "file-finder",
+ "label": "File finder",
+ "bindings": ["CtrlCmd+KEY_P"],
+ "action": {
+ "name": "toggleFileFinder",
+ "params": true
+ }
+ }
+]
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index a12e637616a..e8b51f2b516 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -75,4 +75,8 @@ export default {
},
});
},
+ lastCommitPipelines({ getters }) {
+ const commitSha = getters.lastCommit.id;
+ return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha);
+ },
};
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index c6ba679d99c..1a98b42761e 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,3 +1,4 @@
+import $ from 'jquery';
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
@@ -32,6 +33,19 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
+export const toggleRightPanelCollapsed = ({ dispatch, state }, e = undefined) => {
+ if (e) {
+ $(e.currentTarget)
+ .tooltip('hide')
+ .blur();
+ }
+
+ dispatch('setPanelCollapsedStatus', {
+ side: 'right',
+ collapsed: !state.rightPanelCollapsed,
+ });
+};
+
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
@@ -60,7 +74,7 @@ export const createTempEntry = (
}
worker.addEventListener('message', ({ data }) => {
- const { file } = data;
+ const { file, parentPath } = data;
worker.terminate();
@@ -76,6 +90,10 @@ export const createTempEntry = (
dispatch('setFileActive', file.path);
}
+ if (parentPath && !state.entries[parentPath].opened) {
+ commit(types.TOGGLE_TREE_OPEN, parentPath);
+ }
+
resolve(file);
});
@@ -104,6 +122,16 @@ export const scrollToTab = () => {
});
};
+export const stageAllChanges = ({ state, commit }) => {
+ commit(types.SET_LAST_COMMIT_MSG, '');
+
+ state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
+};
+
+export const unstageAllChanges = ({ state, commit }) => {
+ state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
+};
+
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
@@ -112,7 +140,39 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
+export const updateActivityBarView = ({ commit }, view) => {
+ commit(types.UPDATE_ACTIVITY_BAR_VIEW, view);
+};
+
+export const setEmptyStateSvgs = ({ commit }, svgs) => {
+ commit(types.SET_EMPTY_STATE_SVGS, svgs);
+};
+
+export const setCurrentBranchId = ({ commit }, currentBranchId) => {
+ commit(types.SET_CURRENT_BRANCH, currentBranchId);
+};
+
+export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
+ commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
+
+ if (file.parentPath) {
+ dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile });
+ }
+};
+
+export const toggleFileFinder = ({ commit }, fileFindVisible) =>
+ commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
+
+export const burstUnusedSeal = ({ state, commit }) => {
+ if (state.unusedSeal) {
+ commit(types.BURST_UNUSED_SEAL);
+ }
+};
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 1a17320a1ea..13aea91d8ba 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -5,6 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import { setPageTitle } from '../utils';
+import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
const path = file.path;
@@ -23,10 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) {
- dispatch('updateViewer', 'diff');
- dispatch('openPendingTab', nextFileToOpen);
+ dispatch('updateViewer', viewerTypes.diff);
+ dispatch('openPendingTab', {
+ file: nextFileToOpen,
+ keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
+ });
} else {
- dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
}
} else if (!state.openFiles.length) {
@@ -60,7 +63,9 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
return service
- .getFileData(file.url)
+ .getFileData(
+ `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
+ )
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
@@ -114,7 +119,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
});
};
-export const changeFileContent = ({ state, commit }, { path, content }) => {
+export const changeFileContent = ({ commit, dispatch, state }, { path, content }) => {
const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { path, content });
@@ -125,6 +130,8 @@ export const changeFileContent = ({ state, commit }, { path, content }) => {
} else if (!file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
+
+ dispatch('burstUnusedSeal', {}, { root: true });
};
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
@@ -153,7 +160,7 @@ export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
-export const discardFileChanges = ({ state, commit }, path) => {
+export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => {
const file = state.entries[path];
commit(types.DISCARD_FILE_CHANGES, path);
@@ -161,17 +168,41 @@ export const discardFileChanges = ({ state, commit }, path) => {
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, path);
+ } else if (getters.activeFile && file.path === getters.activeFile.path) {
+ dispatch('updateDelayViewerUpdated', true)
+ .then(() => {
+ router.push(`/project${file.url}`);
+ })
+ .catch(e => {
+ throw e;
+ });
}
- eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
+ eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.content);
+ eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
};
-export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
- if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
- return false;
+export const stageChange = ({ commit, state }, path) => {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ commit(types.STAGE_CHANGE, path);
+ commit(types.SET_LAST_COMMIT_MSG, '');
+
+ if (stagedFile) {
+ eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
}
+};
+
+export const unstageChange = ({ commit }, path) => {
+ commit(types.UNSTAGE_CHANGE, path);
+};
+
+export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
+ if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false;
+
+ state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
- commit(types.ADD_PENDING_TAB, { file });
+ commit(types.ADD_PENDING_TAB, { file, keyPrefix });
dispatch('scrollToTab');
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 4eb23b2ee0f..cece9154c82 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,6 +1,11 @@
+import Visibility from 'visibilityjs';
import flash from '~/flash';
+import { __ } from '~/locale';
import service from '../../services';
import * as types from '../mutation_types';
+import Poll from '../../../lib/utils/poll';
+
+let eTagPoll;
export const getProjectData = (
{ commit, state, dispatch },
@@ -21,7 +26,7 @@ export const getProjectData = (
})
.catch(() => {
flash(
- 'Error loading project data. Please try again.',
+ __('Error loading project data. Please try again.'),
'alert',
document,
null,
@@ -55,12 +60,11 @@ export const getBranchData = (
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data);
})
.catch(() => {
flash(
- 'Error loading branch data. Please try again.',
+ __('Error loading branch data. Please try again.'),
'alert',
document,
null,
@@ -73,3 +77,75 @@ export const getBranchData = (
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
+
+export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) =>
+ service
+ .getBranchData(projectId, branchId)
+ .then(({ data }) => {
+ commit(types.SET_BRANCH_COMMIT, {
+ projectId,
+ branchId,
+ commit: data.commit,
+ });
+ })
+ .catch(() => {
+ flash(__('Error loading last commit.'), 'alert', document, null, false, true);
+ });
+
+export const pollSuccessCallBack = ({ commit, state, dispatch }, { data }) => {
+ if (data.pipelines && data.pipelines.length) {
+ const lastCommitHash =
+ state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id;
+ const lastCommitPipeline = data.pipelines.find(
+ pipeline => pipeline.commit.id === lastCommitHash,
+ );
+ commit(types.SET_LAST_COMMIT_PIPELINE, {
+ projectId: state.currentProjectId,
+ branchId: state.currentBranchId,
+ pipeline: lastCommitPipeline || {},
+ });
+ }
+
+ return data;
+};
+
+export const pipelinePoll = ({ getters, dispatch }) => {
+ eTagPoll = new Poll({
+ resource: service,
+ method: 'lastCommitPipelines',
+ data: {
+ getters,
+ },
+ successCallback: ({ data }) => dispatch('pollSuccessCallBack', { data }),
+ errorCallback: () => {
+ flash(
+ __('Something went wrong while fetching the latest pipeline status.'),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ eTagPoll.restart();
+ } else {
+ eTagPoll.stop();
+ }
+ });
+};
+
+export const stopPipelinePolling = () => {
+ eTagPoll.stop();
+};
+
+export const restartPipelinePolling = () => {
+ eTagPoll.restart();
+};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index a77cdbc13c8..b239a605371 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,3 +1,6 @@
+import { getChangesCountForFiles, filePathMatches } from './utils';
+import { activityBarViews } from '../constants';
+
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
@@ -28,10 +31,61 @@ export const currentMergeRequest = state => {
return null;
};
-// eslint-disable-next-line no-confusing-arrow
-export const currentIcon = state =>
- state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
+export const currentProject = state => state.projects[state.currentProjectId];
+
+export const currentTree = state =>
+ state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
-export const hasChanges = state => !!state.changedFiles.length;
+export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId;
+
+export const allBlobs = state =>
+ Object.keys(state.entries)
+ .reduce((acc, key) => {
+ const entry = state.entries[key];
+
+ if (entry.type === 'blob') {
+ acc.push(entry);
+ }
+
+ return acc;
+ }, [])
+ .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+
+export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
+export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
+
+export const lastOpenedFile = state =>
+ [...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0];
+
+export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit;
+export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
+export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
+
+export const someUncommitedChanges = state =>
+ !!(state.changedFiles.length || state.stagedFiles.length);
+
+export const getChangesInFolder = state => path => {
+ const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
+ const stagedFilesCount = state.stagedFiles.filter(
+ f => filePathMatches(f, path) && !getChangedFile(state)(f.path),
+ ).length;
+
+ return changedFilesCount + stagedFilesCount;
+};
+
+export const getUnstagedFilesCountForPath = state => path =>
+ getChangesCountForFiles(state.changedFiles, path);
+
+export const getStagedFilesCountForPath = state => path =>
+ getChangesCountForFiles(state.stagedFiles, path);
+
+export const lastCommit = (state, getters) => {
+ const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+
+ return branch ? branch.commit : null;
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index 7c82ce7976b..699710055e3 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -5,6 +5,7 @@ import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import commitModule from './modules/commit';
+import pipelines from './modules/pipelines';
Vue.use(Vuex);
@@ -15,5 +16,6 @@ export default new Vuex.Store({
getters,
modules: {
commit: commitModule,
+ pipelines,
},
});
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 367c45f7e2d..cd25c3060f2 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -8,6 +8,7 @@ import router from '../../../ide_router';
import service from '../../../services';
import * as types from './mutation_types';
import * as consts from './constants';
+import { activityBarViews } from '../../../constants';
import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => {
@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) =>
export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters },
- { data, branch },
+ { data },
) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
@@ -98,23 +99,14 @@ export const updateFilesAfterCommit = (
{ root: true },
);
- rootState.changedFiles.forEach(entry => {
- commit(
- rootTypes.SET_LAST_COMMIT_DATA,
- {
- entry,
- lastCommit,
- },
- { root: true },
- );
-
- eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
+ rootState.stagedFiles.forEach(file => {
+ const changedFile = rootState.changedFiles.find(f => f.path === file.path);
commit(
- rootTypes.SET_FILE_RAW_DATA,
+ rootTypes.UPDATE_FILE_AFTER_COMMIT,
{
- file: entry,
- raw: entry.content,
+ file,
+ lastCommit,
},
{ root: true },
);
@@ -122,23 +114,22 @@ export const updateFilesAfterCommit = (
commit(
rootTypes.TOGGLE_FILE_CHANGED,
{
- file: entry,
+ file,
changed: false,
},
{ root: true },
);
- });
- commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
+ dispatch('updateTempFlagForEntry', { file, tempFile: false }, { root: true });
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
- router.push(
- `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
- );
- }
+ eventHub.$emit(`editor.update.model.content.${file.key}`, {
+ content: file.content,
+ changed: !!changedFile,
+ });
+ });
};
-export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => {
+export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
@@ -184,8 +175,52 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
{ root: true },
);
}
+
+ commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
+
+ setTimeout(() => {
+ commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
+ }, 5000);
})
- .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
+ .then(() => {
+ if (rootGetters.lastOpenedFile) {
+ dispatch(
+ 'openPendingTab',
+ {
+ file: rootGetters.lastOpenedFile,
+ },
+ { root: true },
+ )
+ .then(changeViewer => {
+ if (changeViewer) {
+ dispatch('updateViewer', 'diff', { root: true });
+ }
+ })
+ .catch(e => {
+ throw e;
+ });
+ } else {
+ dispatch('updateActivityBarView', activityBarViews.edit, { root: true });
+ dispatch('updateViewer', 'editor', { root: true });
+
+ router.push(
+ `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${
+ rootGetters.activeFile.path
+ }`,
+ );
+ }
+ })
+ .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
+ .then(() =>
+ dispatch(
+ 'refreshLastCommitData',
+ {
+ projectId: rootState.currentProjectId,
+ branchId: rootState.currentBranchId,
+ },
+ { root: true },
+ ),
+ );
})
.catch(err => {
let errMsg = __('Error committing changes. Please try again.');
@@ -198,3 +233,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(types.UPDATE_LOADING, false);
});
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index f7cdd6adb0c..d01060201f2 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,12 +1,17 @@
import * as consts from './constants';
-export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading;
+const BRANCH_SUFFIX_COUNT = 5;
+
+export const discardDraftButtonDisabled = state =>
+ state.commitMessage === '' || state.submitCommitLoading;
export const commitButtonDisabled = (state, getters, rootState) =>
- getters.discardDraftButtonDisabled || !rootState.changedFiles.length;
+ getters.discardDraftButtonDisabled || !rootState.stagedFiles.length;
export const newBranchName = (state, _, rootState) =>
- `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`;
+ `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(
+ -BRANCH_SUFFIX_COUNT,
+ )}`;
export const branchName = (state, getters, rootState) => {
if (
@@ -22,3 +27,6 @@ export const branchName = (state, getters, rootState) => {
return rootState.currentBranchId;
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
new file mode 100644
index 00000000000..07f7b201f2e
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -0,0 +1,49 @@
+import { __ } from '../../../../locale';
+import Api from '../../../../api';
+import flash from '../../../../flash';
+import * as types from './mutation_types';
+
+export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
+export const receiveLatestPipelineError = ({ commit }) => {
+ flash(__('There was an error loading latest pipeline'));
+ commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
+};
+export const receiveLatestPipelineSuccess = ({ commit }, pipeline) =>
+ commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline);
+
+export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => {
+ dispatch('requestLatestPipeline');
+
+ return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' })
+ .then(({ data }) => {
+ dispatch('receiveLatestPipelineSuccess', data.pop());
+ })
+ .catch(() => dispatch('receiveLatestPipelineError'));
+};
+
+export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS);
+export const receiveJobsError = ({ commit }) => {
+ flash(__('There was an error loading jobs'));
+ commit(types.RECEIVE_JOBS_ERROR);
+};
+export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data);
+
+export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => {
+ dispatch('requestJobs');
+
+ Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, {
+ page,
+ })
+ .then(({ data, headers }) => {
+ const nextPage = headers && headers['x-next-page'];
+
+ dispatch('receiveJobsSuccess', data);
+
+ if (nextPage) {
+ dispatch('fetchJobs', nextPage);
+ }
+ })
+ .catch(() => dispatch('receiveJobsError'));
+};
+
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
new file mode 100644
index 00000000000..d6c91f5b64d
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -0,0 +1,7 @@
+export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
+
+export const failedJobs = state =>
+ state.stages.reduce(
+ (acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')),
+ [],
+ );
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/index.js b/app/assets/javascripts/ide/stores/modules/pipelines/index.js
new file mode 100644
index 00000000000..b44c3141b81
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/index.js
@@ -0,0 +1,12 @@
+import state from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+import * as getters from './getters';
+
+export default {
+ namespaced: true,
+ state: state(),
+ actions,
+ mutations,
+ getters,
+};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
new file mode 100644
index 00000000000..6b5701670a6
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
@@ -0,0 +1,7 @@
+export const REQUEST_LATEST_PIPELINE = 'REQUEST_LATEST_PIPELINE';
+export const RECEIVE_LASTEST_PIPELINE_ERROR = 'RECEIVE_LASTEST_PIPELINE_ERROR';
+export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCESS';
+
+export const REQUEST_JOBS = 'REQUEST_JOBS';
+export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
+export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
new file mode 100644
index 00000000000..2b16e57b386
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -0,0 +1,53 @@
+/* eslint-disable no-param-reassign */
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_LATEST_PIPELINE](state) {
+ state.isLoadingPipeline = true;
+ },
+ [types.RECEIVE_LASTEST_PIPELINE_ERROR](state) {
+ state.isLoadingPipeline = false;
+ },
+ [types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) {
+ state.isLoadingPipeline = false;
+
+ if (pipeline) {
+ state.latestPipeline = {
+ id: pipeline.id,
+ status: pipeline.status,
+ };
+ }
+ },
+ [types.REQUEST_JOBS](state) {
+ state.isLoadingJobs = true;
+ },
+ [types.RECEIVE_JOBS_ERROR](state) {
+ state.isLoadingJobs = false;
+ },
+ [types.RECEIVE_JOBS_SUCCESS](state, jobs) {
+ state.isLoadingJobs = false;
+
+ state.stages = jobs.reduce((acc, job) => {
+ let stage = acc.find(s => s.title === job.stage);
+
+ if (!stage) {
+ stage = {
+ title: job.stage,
+ jobs: [],
+ };
+
+ acc.push(stage);
+ }
+
+ stage.jobs = stage.jobs.concat({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ stage: job.stage,
+ duration: job.duration,
+ });
+
+ return acc;
+ }, state.stages);
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
new file mode 100644
index 00000000000..6f22542aaea
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
@@ -0,0 +1,6 @@
+export default () => ({
+ isLoadingPipeline: false,
+ isLoadingJobs: false,
+ latestPipeline: null,
+ stages: [],
+});
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e3f504e5ab0..0a3f8d031c4 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
+export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
@@ -19,8 +20,10 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
+export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
+export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
@@ -51,5 +54,15 @@ export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
+export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
+export const STAGE_CHANGE = 'STAGE_CHANGE';
+export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
+
+export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
+
+export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
+export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
+export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
+export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 5e5eb831662..a257e2ef025 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -4,6 +4,7 @@ import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
+import { sortTree } from './utils';
export default {
[types.SET_INITIAL_DATA](state, data) {
@@ -49,6 +50,11 @@ export default {
lastCommitMsg,
});
},
+ [types.CLEAR_STAGED_CHANGES](state) {
+ Object.assign(state, {
+ stagedFiles: [],
+ });
+ },
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
@@ -68,7 +74,7 @@ export default {
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
);
Object.assign(foundEntry, {
- tree: foundEntry.tree.concat(tree),
+ tree: sortTree(foundEntry.tree.concat(tree)),
});
}
@@ -81,10 +87,16 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
+ tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)),
});
}
},
+ [types.UPDATE_TEMP_FLAG](state, { path, tempFile }) {
+ Object.assign(state.entries[path], {
+ tempFile,
+ changed: tempFile,
+ });
+ },
[types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, {
viewer,
@@ -95,6 +107,47 @@ export default {
delayViewerUpdated,
});
},
+ [types.UPDATE_ACTIVITY_BAR_VIEW](state, currentActivityView) {
+ Object.assign(state, {
+ currentActivityView,
+ });
+ },
+ [types.SET_EMPTY_STATE_SVGS](
+ state,
+ { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
+ ) {
+ Object.assign(state, {
+ emptyStateSvgPath,
+ noChangesStateSvgPath,
+ committedStateSvgPath,
+ });
+ },
+ [types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
+ Object.assign(state, {
+ fileFindVisible,
+ });
+ },
+ [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
+ const changedFile = state.changedFiles.find(f => f.path === file.path);
+
+ Object.assign(state.entries[file.path], {
+ raw: file.content,
+ changed: !!changedFile,
+ staged: false,
+ lastCommit: Object.assign(state.entries[file.path].lastCommit, {
+ id: lastCommit.commit.id,
+ url: lastCommit.commit_path,
+ message: lastCommit.commit.message,
+ author: lastCommit.commit.author_name,
+ updatedAt: lastCommit.commit.authored_date,
+ }),
+ });
+ },
+ [types.BURST_UNUSED_SEAL](state) {
+ Object.assign(state, {
+ unusedSeal: false,
+ });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
index 2972ba5e38e..f17ec4da308 100644
--- a/app/assets/javascripts/ide/stores/mutations/branch.js
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -14,6 +14,10 @@ export default {
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
+ commit: {
+ ...branch.commit,
+ pipeline: {},
+ },
},
},
});
@@ -23,4 +27,14 @@ export default {
workingReference: reference,
});
},
+ [types.SET_BRANCH_COMMIT](state, { projectId, branchId, commit }) {
+ Object.assign(state.projects[projectId].branches[branchId], {
+ commit,
+ });
+ },
+ [types.SET_LAST_COMMIT_PIPELINE](state, { projectId, branchId, pipeline }) {
+ Object.assign(state.projects[projectId].branches[branchId].commit, {
+ pipeline,
+ });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index eeb14b5490c..13f123b6630 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,9 +1,11 @@
+/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], {
active,
+ lastOpenedAt: new Date().getTime(),
});
if (active && !state.entries[path].pending) {
@@ -57,7 +59,9 @@ export default {
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
- const changed = content !== state.entries[path].raw;
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+ const rawContent = stagedFile ? stagedFile.content : state.entries[path].raw;
+ const changed = content !== rawContent;
Object.assign(state.entries[path], {
content,
@@ -91,8 +95,10 @@ export default {
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
Object.assign(state.entries[path], {
- content: state.entries[path].raw,
+ content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
});
},
@@ -106,38 +112,82 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
+ [types.STAGE_CHANGE](state, path) {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ Object.assign(state, {
+ changedFiles: state.changedFiles.filter(f => f.path !== path),
+ entries: Object.assign(state.entries, {
+ [path]: Object.assign(state.entries[path], {
+ staged: true,
+ changed: false,
+ }),
+ }),
+ });
+
+ if (stagedFile) {
+ Object.assign(stagedFile, {
+ ...state.entries[path],
+ });
+ } else {
+ Object.assign(state, {
+ stagedFiles: state.stagedFiles.concat({
+ ...state.entries[path],
+ }),
+ });
+ }
+ },
+ [types.UNSTAGE_CHANGE](state, path) {
+ const changedFile = state.changedFiles.find(f => f.path === path);
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ if (!changedFile && stagedFile) {
+ Object.assign(state.entries[path], {
+ ...stagedFile,
+ key: state.entries[path].key,
+ active: state.entries[path].active,
+ opened: state.entries[path].opened,
+ changed: true,
+ });
+
+ Object.assign(state, {
+ changedFiles: state.changedFiles.concat(state.entries[path]),
+ });
+ }
+
+ Object.assign(state, {
+ stagedFiles: state.stagedFiles.filter(f => f.path !== path),
+ entries: Object.assign(state.entries, {
+ [path]: Object.assign(state.entries[path], {
+ staged: false,
+ }),
+ }),
+ });
+ },
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(state.entries[file.path], {
changed,
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
- const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
- let openFiles = state.openFiles.map(f =>
- Object.assign(f, { active: f.path === file.path, opened: false }),
+ state.entries[file.path].opened = false;
+ state.entries[file.path].active = false;
+ state.entries[file.path].lastOpenedAt = new Date().getTime();
+ state.openFiles.forEach(f =>
+ Object.assign(f, {
+ opened: false,
+ active: false,
+ }),
);
-
- if (!pendingTab) {
- const openFile = openFiles.find(f => f.path === file.path);
-
- openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
- if (!f) return acc;
-
- if (f.path === file.path) {
- return acc.concat({
- ...f,
- active: true,
- pending: true,
- opened: true,
- key: `${keyPrefix}-${f.key}`,
- });
- }
-
- return acc.concat(f);
- }, []);
- }
-
- Object.assign(state, { openFiles });
+ state.openFiles = [
+ {
+ ...file,
+ key: `${keyPrefix}-${file.key}`,
+ pending: true,
+ opened: true,
+ active: true,
+ },
+ ];
},
[types.REMOVE_PENDING_TAB](state, file) {
Object.assign(state, {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index e5cc8814000..e7411f16a4f 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,8 +1,11 @@
+import { activityBarViews, viewerTypes } from '../constants';
+
export default () => ({
currentProjectId: '',
currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [],
+ stagedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
@@ -15,6 +18,9 @@ export default () => ({
rightPanelCollapsed: false,
panelResizing: false,
entries: {},
- viewer: 'editor',
+ viewer: viewerTypes.edit,
delayViewerUpdated: false,
+ currentActivityView: activityBarViews.edit,
+ unusedSeal: true,
+ fileFindVisible: false,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 05a019de54f..e0b9766fbee 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -15,6 +15,7 @@ export const dataStructure = () => ({
opened: false,
active: false,
changed: false,
+ staged: false,
lastCommitPath: '',
lastCommit: {
id: '',
@@ -41,6 +42,9 @@ export const dataStructure = () => ({
viewMode: 'edit',
previewMode: null,
size: 0,
+ parentPath: null,
+ lastOpenedAt: 0,
+ mrChange: null,
});
export const decorateData = entity => {
@@ -63,6 +67,7 @@ export const decorateData = entity => {
previewMode,
file_lock,
html,
+ parentPath = '',
} = entity;
return {
@@ -86,6 +91,7 @@ export const decorateData = entity => {
previewMode,
file_lock,
html,
+ parentPath,
};
};
@@ -101,7 +107,7 @@ export const setPageTitle = title => {
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
- actions: rootState.changedFiles.map(f => ({
+ actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
@@ -119,8 +125,8 @@ const sortTreesByTypeAndName = (a, b) => {
} else if (a.type === 'blob' && b.type === 'tree') {
return 1;
}
- if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
- if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
+ if (a.name < b.name) return -1;
+ if (a.name > b.name) return 1;
return 0;
};
@@ -132,3 +138,9 @@ export const sortTree = sortedTree =>
}),
)
.sort(sortTreesByTypeAndName);
+
+export const filePathMatches = (f, path) =>
+ f.path.replace(new RegExp(`${f.name}$`), '').indexOf(`${path}/`) === 0;
+
+export const getChangesCountForFiles = (files, path) =>
+ files.filter(f => filePathMatches(f, path)).length;
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index a1673276900..0a1c253c637 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -6,6 +6,7 @@ self.addEventListener('message', e => {
const treeList = [];
let file;
+ let parentPath;
const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/');
const blobName = pathSplit.pop().trim();
@@ -17,18 +18,21 @@ self.addEventListener('message', e => {
const foundEntry = acc[folderPath];
if (!foundEntry) {
+ parentPath = parentFolder ? parentFolder.path : null;
+
const tree = decorateData({
projectId,
branchId,
id: folderPath,
name: folderName,
path: folderPath,
- url: `/${projectId}/tree/${branchId}/${folderPath}/`,
+ url: `/${projectId}/tree/${branchId}/-/${folderPath}/`,
type: 'tree',
parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
changed: tempFile,
opened: tempFile,
+ parentPath,
});
Object.assign(acc, {
@@ -52,13 +56,15 @@ self.addEventListener('message', e => {
if (blobName !== '') {
const fileFolder = acc[pathSplit.join('/')];
+ parentPath = fileFolder ? fileFolder.path : null;
+
file = decorateData({
projectId,
branchId,
id: path,
name: blobName,
path,
- url: `/${projectId}/blob/${branchId}/${path}`,
+ url: `/${projectId}/blob/${branchId}/-/${path}`,
type: 'blob',
parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
@@ -66,6 +72,7 @@ self.addEventListener('message', e => {
content,
base64,
previewMode: viewerInformationForPath(blobName),
+ parentPath,
});
Object.assign(acc, {
@@ -86,5 +93,6 @@ self.addEventListener('message', e => {
entries,
treeList: sortTree(treeList),
file,
+ parentPath,
});
});
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 7470d634b99..f3d722409b0 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -30,10 +30,10 @@ export default class IssuableContext {
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
- $block.find('.value').show();
+ $block.find('.value:not(.dont-hide)').show();
} else {
$selectbox.show();
- $block.find('.value').hide();
+ $block.find('.value:not(.dont-hide)').hide();
}
if ($selectbox.is(':visible')) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index bb8b3d91e40..90d4e19e90b 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -30,7 +30,7 @@ export default class IssuableForm {
}
this.initAutosave();
- this.form.on('submit', this.handleSubmit);
+ this.form.on('submit:success', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip();
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index a539506bce2..7ef5e679881 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -51,7 +51,7 @@
<template>
<div class="prepend-top-default append-bottom-default clearfix">
<button
- class="btn btn-save pull-left"
+ class="btn btn-save float-left"
:class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
type="submit"
:disabled="formState.updateLoading || !isSubmitEnabled"
@@ -64,14 +64,14 @@
</i>
</button>
<button
- class="btn btn-default pull-right"
+ class="btn btn-default float-right"
type="button"
@click="closeForm">
Cancel
</button>
<button
v-if="shouldShowDeleteButton"
- class="btn btn-danger pull-right append-right-default"
+ class="btn btn-danger float-right append-right-default"
:class="{ disabled: deleteLoading }"
type="button"
:disabled="deleteLoading"
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 779705e19ac..ab8bd34762f 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -84,7 +84,7 @@
<div
:class="{
'col-sm-8 col-lg-9': hasIssuableTemplates,
- 'col-xs-12': !hasIssuableTemplates,
+ 'col-12': !hasIssuableTemplates,
}"
>
<title-field
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index ace45e9dd29..c67bd7fb0c6 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import _ from 'underscore';
+import StickyFill from 'stickyfilljs';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
@@ -82,17 +83,11 @@ export default class Job {
/**
If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not
- then we default back to Bootstraps affix
+ then we use a polyfill
**/
if (this.$topBar.css('position') !== 'static') return;
- const offsetTop = this.$buildTrace.offset().top;
-
- this.$topBar.affix({
- offset: {
- top: offsetTop,
- },
- });
+ StickyFill.add(this.$topBar);
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index 357bc9aab17..c1044f4cd42 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -1,82 +1,94 @@
<script>
- import ciHeader from '../../vue_shared/components/header_ci_component.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import ciHeader from '../../vue_shared/components/header_ci_component.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import callout from '../../vue_shared/components/callout.vue';
- export default {
- name: 'JobHeaderSection',
- components: {
- ciHeader,
- loadingIcon,
+export default {
+ name: 'JobHeaderSection',
+ components: {
+ ciHeader,
+ loadingIcon,
+ callout,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
+ isLoading: {
+ type: Boolean,
+ required: true,
},
- data() {
- return {
- actions: this.getActions(),
- };
+ },
+ data() {
+ return {
+ actions: this.getActions(),
+ };
+ },
+ computed: {
+ status() {
+ return this.job && this.job.status;
},
- computed: {
- status() {
- return this.job && this.job.status;
- },
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length;
- },
- /**
- * When job has not started the key will be `false`
- * When job started the key will be a string with a date.
- */
- jobStarted() {
- return !this.job.started === false;
- },
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length;
},
- watch: {
- job() {
- this.actions = this.getActions();
- },
+ shouldRenderReason() {
+ return !!(this.job.status && this.job.callout_message);
},
- methods: {
- getActions() {
- const actions = [];
+ /**
+ * When job has not started the key will be `false`
+ * When job started the key will be a string with a date.
+ */
+ jobStarted() {
+ return !this.job.started === false;
+ },
+ },
+ watch: {
+ job() {
+ this.actions = this.getActions();
+ },
+ },
+ methods: {
+ getActions() {
+ const actions = [];
- if (this.job.new_issue_path) {
- actions.push({
- label: 'New issue',
- path: this.job.new_issue_path,
- cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
- type: 'link',
- });
- }
- return actions;
- },
+ if (this.job.new_issue_path) {
+ actions.push({
+ label: 'New issue',
+ path: this.job.new_issue_path,
+ cssClass: 'js-new-issue btn btn-new btn-inverted d-none d-md-block d-lg-block d-xl-block',
+ type: 'link',
+ });
+ }
+ return actions;
},
- };
+ },
+};
</script>
<template>
- <div class="js-build-header build-header top-area">
- <ci-header
- v-if="shouldRenderContent"
- :status="status"
- item-name="Job"
- :item-id="job.id"
- :time="job.created_at"
- :user="job.user"
- :actions="actions"
- :has-sidebar-button="true"
- :should-render-triggered-label="jobStarted"
- />
- <loading-icon
- v-if="isLoading"
- size="2"
- class="prepend-top-default append-bottom-default"
+ <header>
+ <div class="js-build-header build-header top-area">
+ <ci-header
+ v-if="shouldRenderContent"
+ :status="status"
+ item-name="Job"
+ :item-id="job.id"
+ :time="job.created_at"
+ :user="job.user"
+ :actions="actions"
+ :has-sidebar-button="true"
+ :should-render-triggered-label="jobStarted"
+ />
+ <loading-icon
+ v-if="isLoading"
+ size="2"
+ class="prepend-top-default append-bottom-default"
+ />
+ </div>
+
+ <callout
+ v-if="shouldRenderReason"
+ :message="job.callout_message"
/>
- </div>
+ </header>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index dfe87d89a39..83560a8ff0e 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -39,7 +39,7 @@
<span
v-if="hasHelpURL"
- class="help-button pull-right"
+ class="help-button float-right"
>
<a
:href="helpUrl"
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index af47056d98f..82fec7e936b 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -1,80 +1,119 @@
<script>
- import detailRow from './sidebar_detail_row.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import timeagoMixin from '../../vue_shared/mixins/timeago';
- import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
+import detailRow from './sidebar_detail_row.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import timeagoMixin from '../../vue_shared/mixins/timeago';
+import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
- export default {
- name: 'SidebarDetailsBlock',
- components: {
- detailRow,
- loadingIcon,
+export default {
+ name: 'SidebarDetailsBlock',
+ components: {
+ detailRow,
+ loadingIcon,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- runnerHelpUrl: {
- type: String,
- required: false,
- default: '',
- },
+ isLoading: {
+ type: Boolean,
+ required: true,
},
- computed: {
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length > 0;
- },
- coverage() {
- return `${this.job.coverage}%`;
- },
- duration() {
- return timeIntervalInWords(this.job.duration);
- },
- queued() {
- return timeIntervalInWords(this.job.queued);
- },
- runnerId() {
- return `#${this.job.runner.id}`;
- },
- hasTimeout() {
- return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
- },
- timeout() {
- if (this.job.metadata == null) {
- return '';
- }
+ canUserRetry: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length > 0;
+ },
+ coverage() {
+ return `${this.job.coverage}%`;
+ },
+ duration() {
+ return timeIntervalInWords(this.job.duration);
+ },
+ queued() {
+ return timeIntervalInWords(this.job.queued);
+ },
+ runnerId() {
+ return `${this.job.runner.description} (#${this.job.runner.id})`;
+ },
+ retryButtonClass() {
+ let className = 'js-retry-button pull-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
+ className +=
+ this.job.status && this.job.recoverable
+ ? ' btn-primary'
+ : ' btn-inverted-secondary';
+ return className;
+ },
+ hasTimeout() {
+ return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
+ },
+ timeout() {
+ if (this.job.metadata == null) {
+ return '';
+ }
- let t = this.job.metadata.timeout_human_readable;
- if (this.job.metadata.timeout_source !== '') {
- t += ` (from ${this.job.metadata.timeout_source})`;
- }
+ let t = this.job.metadata.timeout_human_readable;
+ if (this.job.metadata.timeout_source !== '') {
+ t += ` (from ${this.job.metadata.timeout_source})`;
+ }
- return t;
- },
- renderBlock() {
- return this.job.merge_request ||
- this.job.duration ||
- this.job.finished_data ||
- this.job.erased_at ||
- this.job.queued ||
- this.job.runner ||
- this.job.coverage ||
- this.job.tags.length ||
- this.job.cancel_path;
- },
+ return t;
},
- };
+ renderBlock() {
+ return (
+ this.job.merge_request ||
+ this.job.duration ||
+ this.job.finished_data ||
+ this.job.erased_at ||
+ this.job.queued ||
+ this.job.runner ||
+ this.job.coverage ||
+ this.job.tags.length ||
+ this.job.cancel_path
+ );
+ },
+ },
+};
</script>
<template>
<div>
+ <div class="block">
+ <strong class="inline prepend-top-8">
+ {{ job.name }}
+ </strong>
+ <a
+ v-if="canUserRetry"
+ :class="retryButtonClass"
+ :href="job.retry_path"
+ data-method="post"
+ rel="nofollow"
+ >
+ {{ __('Retry') }}
+ </a>
+ <button
+ type="button"
+ :aria-label="__('Toggle Sidebar')"
+ class="btn btn-blank gutter-toggle pull-right
+ d-block d-sm-block d-md-none js-sidebar-build-toggle"
+ >
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-angle-double-right"
+ ></i>
+ </button>
+ </div>
<template v-if="shouldRenderContent">
<div
class="block retry-link"
@@ -85,16 +124,16 @@
class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path"
>
- New issue
+ {{ __('New issue') }}
</a>
<a
- v-if="job.retry_path"
+ v-if="canUserRetry"
class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
- Retry
+ {{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
@@ -103,7 +142,7 @@
v-if="job.merge_request"
>
<span class="build-light-text">
- Merge Request:
+ {{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
@@ -158,7 +197,7 @@
v-if="job.tags.length"
>
<span class="build-light-text">
- Tags:
+ {{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
@@ -178,7 +217,7 @@
data-method="post"
rel="nofollow"
>
- Cancel
+ {{ __('Cancel') }}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index 656676ead91..f2939ad4dbe 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -35,9 +35,11 @@ export default () => {
});
// Sidebar information block
+ const detailsBlockElement = document.getElementById('js-details-block-vue');
+ const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line
new Vue({
- el: '#js-details-block-vue',
+ el: detailsBlockElement,
components: {
detailsBlock,
},
@@ -50,6 +52,7 @@ export default () => {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
+ canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 8013df53f25..a8a314abd6b 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -36,7 +36,7 @@ export default class LabelManager {
const $label = $(`#${$btn.data('domId')}`);
const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
- $tooltip.tooltip('destroy');
+ $tooltip.tooltip('dispose');
_this.toggleLabelPriority($label, action);
_this.toggleEmptyState($label, $btn, action);
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index d0050abb8e9..eafdaf4a672 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -83,7 +83,7 @@ export default class LabelsSelect {
$dropdown.trigger('loading.gl.dropdown');
axios.put(issueUpdateURL, data)
.then(({ data }) => {
- var labelCount, template, labelTooltipTitle, labelTitles;
+ var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
@@ -115,13 +115,12 @@ export default class LabelsSelect {
labelTooltipTitle = labelTitles.join(', ');
}
else {
- labelTooltipTitle = '';
- $sidebarLabelTooltip.tooltip('destroy');
+ labelTooltipTitle = __('Labels');
}
$sidebarLabelTooltip
.attr('title', labelTooltipTitle)
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
$('.has-tooltip', $value).tooltip({
container: 'body'
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 9ff2042475b..8b5445d012b 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -51,7 +51,7 @@ export const rstrip = (val) => {
return val;
};
-export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
+export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('_fixTitle');
export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
const field = $(fieldSelector);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index c3d94d63c13..0ff23bbb061 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -2,10 +2,7 @@ import $ from 'jquery';
import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility';
-import {
- languageCode,
- s__,
-} from '../../locale';
+import { languageCode, s__ } from '../../locale';
window.timeago = timeago;
window.dateFormat = dateFormat;
@@ -17,11 +14,37 @@ window.dateFormat = dateFormat;
*
* @param {Boolean} abbreviated
*/
-const getMonthNames = (abbreviated) => {
+const getMonthNames = abbreviated => {
if (abbreviated) {
- return [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
+ return [
+ s__('Jan'),
+ s__('Feb'),
+ s__('Mar'),
+ s__('Apr'),
+ s__('May'),
+ s__('Jun'),
+ s__('Jul'),
+ s__('Aug'),
+ s__('Sep'),
+ s__('Oct'),
+ s__('Nov'),
+ s__('Dec'),
+ ];
}
- return [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
+ return [
+ s__('January'),
+ s__('February'),
+ s__('March'),
+ s__('April'),
+ s__('May'),
+ s__('June'),
+ s__('July'),
+ s__('August'),
+ s__('September'),
+ s__('October'),
+ s__('November'),
+ s__('December'),
+ ];
};
/**
@@ -29,7 +52,8 @@ const getMonthNames = (abbreviated) => {
* @param {date} date
* @returns {String}
*/
-export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
+export const getDayName = date =>
+ ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
/**
* @example
@@ -55,7 +79,7 @@ export function getTimeago() {
if (!timeagoInstance) {
const localeRemaining = function getLocaleRemaining(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
+ [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
[s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
@@ -73,7 +97,7 @@ export function getTimeago() {
};
const locale = function getLocale(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
+ [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
[s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
@@ -102,7 +126,7 @@ export function getTimeago() {
* For the given element, renders a timeago instance.
* @param {jQuery} $els
*/
-export const renderTimeago = ($els) => {
+export const renderTimeago = $els => {
const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
// timeago.js sets timeouts internally for each timeago value to be updated in real time
@@ -119,7 +143,7 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
if (setTimeago) {
// Recreate with custom template
$(el).tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+ template: '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',
});
}
@@ -141,7 +165,9 @@ export const timeFor = (time, expiredLabel) => {
if (new Date(time) < new Date()) {
return expiredLabel || s__('Timeago|Past due');
}
- return getTimeago().format(time, `${timeagoLanguageCode}-remaining`).trim();
+ return getTimeago()
+ .format(time, `${timeagoLanguageCode}-remaining`)
+ .trim();
};
export const getDayDifference = (a, b) => {
@@ -161,7 +187,7 @@ export const getDayDifference = (a, b) => {
export function timeIntervalInWords(intervalInSeconds) {
const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60);
- const seconds = secondsInteger - (minutes * 60);
+ const seconds = secondsInteger - minutes * 60;
let text = '';
if (minutes >= 1) {
@@ -178,8 +204,34 @@ export function dateInWords(date, abbreviated = false, hideYear = false) {
const month = date.getMonth();
const year = date.getFullYear();
- const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
- const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
+ const monthNames = [
+ s__('January'),
+ s__('February'),
+ s__('March'),
+ s__('April'),
+ s__('May'),
+ s__('June'),
+ s__('July'),
+ s__('August'),
+ s__('September'),
+ s__('October'),
+ s__('November'),
+ s__('December'),
+ ];
+ const monthNamesAbbr = [
+ s__('Jan'),
+ s__('Feb'),
+ s__('Mar'),
+ s__('Apr'),
+ s__('May'),
+ s__('Jun'),
+ s__('Jul'),
+ s__('Aug'),
+ s__('Sep'),
+ s__('Oct'),
+ s__('Nov'),
+ s__('Dec'),
+ ];
const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
@@ -210,7 +262,7 @@ export const monthInWords = (date, abbreviated = false) => {
*
* @param {Date} date
*/
-export const totalDaysInMonth = (date) => {
+export const totalDaysInMonth = date => {
if (!date) {
return 0;
}
@@ -223,12 +275,20 @@ export const totalDaysInMonth = (date) => {
*
* @param {Date} date
*/
-export const getSundays = (date) => {
+export const getSundays = date => {
if (!date) {
return [];
}
- const daysToSunday = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday'];
+ const daysToSunday = [
+ 'Saturday',
+ 'Friday',
+ 'Thursday',
+ 'Wednesday',
+ 'Tuesday',
+ 'Monday',
+ 'Sunday',
+ ];
const month = date.getMonth();
const year = date.getFullYear();
diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js
new file mode 100644
index 00000000000..5e0f9b612a2
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/keycodes.js
@@ -0,0 +1,4 @@
+export const UP_KEY_CODE = 38;
+export const DOWN_KEY_CODE = 40;
+export const ENTER_KEY_CODE = 13;
+export const ESC_KEY_CODE = 27;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index b54ecd2d543..5e786ee6935 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -74,7 +74,11 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace
* @returns {String}
*/
-export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
+export const stripHtml = (string, replace = '') => {
+ if (!string) return string;
+
+ return string.replace(/<[^>]*>/g, replace);
+};
/**
* Converts snake_case string to camelCase
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 2c80baba10b..9803bebfd10 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,22 +1,19 @@
-/* eslint-disable import/first */
/* global $ */
import jQuery from 'jquery';
import Cookies from 'js-cookie';
import svg4everybody from 'svg4everybody';
-// expose common libraries as globals (TODO: remove these)
-window.jQuery = jQuery;
-window.$ = jQuery;
+// bootstrap webpack, common libs, polyfills, and behaviors
+import './webpack';
+import './commons';
+import './behaviors';
// lib/utils
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
-// behaviors
-import './behaviors/';
-
// everything else
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
@@ -31,9 +28,12 @@ import initLogoAnimation from './logo';
import './milestone_select';
import './projects_dropdown';
import initBreadcrumbs from './breadcrumb';
-
import initDispatcher from './dispatcher';
+// expose jQuery as global (TODO: remove these)
+window.jQuery = jQuery;
+window.$ = jQuery;
+
// inject test utilities if necessary
if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
$.fx.off = true;
@@ -46,16 +46,20 @@ document.addEventListener('beforeunload', () => {
// Unbind scroll events
$(document).off('scroll');
// Close any open tooltips
- $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+ $('.has-tooltip, [data-toggle="tooltip"]').tooltip('dispose');
// Close any open popover
- $('[data-toggle="popover"]').popover('destroy');
+ $('[data-toggle="popover"]').popover('dispose');
});
window.addEventListener('hashchange', handleLocationHash);
-window.addEventListener('load', function onLoad() {
- window.removeEventListener('load', onLoad, false);
- handleLocationHash();
-}, false);
+window.addEventListener(
+ 'load',
+ function onLoad() {
+ window.removeEventListener('load', onLoad, false);
+ handleLocationHash();
+ },
+ false,
+);
gl.lazyLoader = new LazyLoader({
scrollContainer: window,
@@ -89,9 +93,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (bootstrapBreakpoint === 'xs') {
const $rightSidebar = $('aside.right-sidebar, .layout-page');
- $rightSidebar
- .removeClass('right-sidebar-expanded')
- .addClass('right-sidebar-collapsed');
+ $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
}
// prevent default action for disabled buttons
@@ -108,7 +110,8 @@ document.addEventListener('DOMContentLoaded', () => {
addSelectOnFocusBehaviour('.js-select-on-focus');
$('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() {
- $(this).tooltip('destroy')
+ $(this)
+ .tooltip('dispose')
.closest('li')
.fadeOut();
});
@@ -118,7 +121,9 @@ document.addEventListener('DOMContentLoaded', () => {
});
$('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() {
- $(this).closest('tr').fadeOut();
+ $(this)
+ .closest('tr')
+ .fadeOut();
});
// Initialize select2 selects
@@ -136,9 +141,9 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Initialize tooltips
- $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
+ trigger: 'hover',
placement(tip, el) {
return $(el).data('placement') || 'bottom';
},
@@ -155,7 +160,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Form submitter
$('.trigger-submit').on('change', function triggerSubmitCallback() {
- $(this).parents('form').submit();
+ $(this)
+ .parents('form')
+ .submit();
});
localTimeAgo($('abbr.timeago, .js-timeago'), true);
@@ -189,7 +196,7 @@ document.addEventListener('DOMContentLoaded', () => {
$container.remove();
});
- $('.navbar-toggle').on('click', () => {
+ $('.navbar-toggler').on('click', () => {
$('.header-content').toggleClass('menu-expanded');
gl.lazyLoader.loadCheck();
});
@@ -204,9 +211,15 @@ document.addEventListener('DOMContentLoaded', () => {
$this.toggleClass('active');
if ($this.hasClass('active')) {
- notesHolders.show().find('.hide, .content').show();
+ notesHolders
+ .show()
+ .find('.hide, .content')
+ .show();
} else {
- notesHolders.hide().find('.content').hide();
+ notesHolders
+ .hide()
+ .find('.content')
+ .hide();
}
$(document).trigger('toggle.comments');
@@ -247,9 +260,11 @@ document.addEventListener('DOMContentLoaded', () => {
const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) {
- flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => {
- removeFlashClickListener(flashEl);
- });
+ flashContainer
+ .querySelectorAll('.flash-alert, .flash-notice, .flash-success')
+ .forEach(flashEl => {
+ removeFlashClickListener(flashEl);
+ });
}
initDispatcher();
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index db1d09eb2f2..70f185e3656 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -351,7 +351,7 @@ import Cookies from 'js-cookie';
},
getCommitButtonText() {
- const initial = 'Commit conflict resolution';
+ const initial = 'Commit to source branch';
const inProgress = 'Committing...';
return this.state ? this.state.isSubmitting ? inProgress : initial : initial;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 3f84f4b9499..3548c07aea8 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -362,7 +362,7 @@ export default class MergeRequestTabs {
//
// status - Boolean, true to show, false to hide
toggleLoading(status) {
- $('.mr-loading-status .loading').toggle(status);
+ $('.mr-loading-status .loading').toggleClass('hidden', status);
}
diffViewType() {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index d0a2b27b0e6..f8b3d3061f0 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -4,6 +4,7 @@
import $ from 'jquery';
import _ from 'underscore';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
@@ -11,7 +12,8 @@ import ModalStore from './boards/stores/modal_store';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
if (currentProject !== null) {
- this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
+ this.currentProject =
+ typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
this.init(els, options);
@@ -25,7 +27,10 @@ export default class MilestoneSelect {
}
$els.each((i, dropdown) => {
- let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
+ let milestoneLinkNoneTemplate,
+ milestoneLinkTemplate,
+ selectedMilestone,
+ selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones');
@@ -45,46 +50,47 @@ export default class MilestoneSelect {
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
const $value = $block.find('.value');
const $loading = $block.find('.block-loading').fadeOut();
- selectedMilestoneDefault = (showAny ? '' : null);
- selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
+ selectedMilestoneDefault = showAny ? '' : null;
+ selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault;
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
- milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
+ milestoneLinkTemplate = _.template(
+ '<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
+ );
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
- collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
- data: (term, callback) => axios.get(milestonesUrl)
- .then(({ data }) => {
+ data: (term, callback) =>
+ axios.get(milestonesUrl).then(({ data }) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
- id: 0,
- name: '',
- title: 'Any Milestone'
+ id: null,
+ name: null,
+ title: 'Any Milestone',
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
- title: 'No Milestone'
+ title: 'No Milestone',
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
- title: 'Upcoming'
+ title: 'Upcoming',
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
- title: 'Started'
+ title: 'Started',
});
}
if (extraOptions.length) {
@@ -106,7 +112,7 @@ export default class MilestoneSelect {
`,
filterable: true,
search: {
- fields: ['title']
+ fields: ['title'],
},
selectable: true,
toggleLabel: (selected, el, e) => {
@@ -119,7 +125,7 @@ export default class MilestoneSelect {
defaultLabel: defaultLabel,
fieldName: $dropdown.data('fieldName'),
text: milestone => _.escape(milestone.title),
- id: (milestone) => {
+ id: milestone => {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
@@ -131,7 +137,7 @@ export default class MilestoneSelect {
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
- opened: (e) => {
+ opened: e => {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
@@ -140,7 +146,7 @@ export default class MilestoneSelect {
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: (clickEvent) => {
+ clicked: clickEvent => {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
@@ -155,11 +161,14 @@ export default class MilestoneSelect {
const page = $('body').attr('data-page');
const isIssueIndex = page === 'projects:issues:index';
- const isMRIndex = (page === page && page === 'projects:merge_requests:index');
- const isSelecting = (selected.name !== selectedMilestone);
+ const isMRIndex = page === page && page === 'projects:merge_requests:index';
+ const isSelecting = selected.name !== selectedMilestone;
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
- if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ if (
+ $dropdown.hasClass('js-filter-bulk-update') ||
+ $dropdown.hasClass('js-issuable-form-dropdown')
+ ) {
e.preventDefault();
return;
}
@@ -177,10 +186,13 @@ export default class MilestoneSelect {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1 && isSelecting) {
- gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
- id: selected.id,
- title: selected.name
- }));
+ gl.issueBoards.boardStoreIssueSet(
+ 'milestone',
+ new ListMilestone({
+ id: selected.id,
+ title: selected.name,
+ }),
+ );
} else {
gl.issueBoards.boardStoreIssueDelete('milestone');
}
@@ -188,7 +200,8 @@ export default class MilestoneSelect {
$dropdown.trigger('loading.gl.dropdown');
$loading.removeClass('hidden').fadeIn();
- gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ gl.issueBoards.BoardsStore.detail.issue
+ .update($dropdown.attr('data-issue-update'))
.then(() => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
@@ -203,7 +216,8 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
- return axios.put(issueUpdateURL, data)
+ return axios
+ .put(issueUpdateURL, data)
.then(({ data }) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
@@ -214,17 +228,26 @@ export default class MilestoneSelect {
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
- return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
+ return $sidebarCollapsedValue
+ .attr(
+ 'data-original-title',
+ `${data.milestone.name}<br />${data.milestone.remaining}`,
+ )
+ .find('span')
+ .text(data.milestone.title);
} else {
$value.html(milestoneLinkNoneTemplate);
- return $sidebarCollapsedValue.find('span').text('No');
+ return $sidebarCollapsedValue
+ .attr('data-original-title', __('Milestone'))
+ .find('span')
+ .text(__('None'));
}
})
.catch(() => {
$loading.fadeOut();
});
}
- }
+ },
});
});
}
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index 01399de4c62..f8257b6abab 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-new */
-
import $ from 'jquery';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -62,7 +60,7 @@ export default class MiniPipelineGraph {
*/
renderBuildsList(stageContainer, data) {
const dropdownContainer = stageContainer.parentElement.querySelector(
- `${this.dropdownListSelector} .js-builds-dropdown-list`,
+ `${this.dropdownListSelector} .js-builds-dropdown-list ul`,
);
dropdownContainer.innerHTML = data;
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index f93b1da4f58..de6755e0414 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -81,9 +81,8 @@ export default {
time: new Date(),
value: 0,
},
- currentDataIndex: 0,
currentXCoordinate: 0,
- currentFlagPosition: 0,
+ currentCoordinates: [],
showFlag: false,
showFlagContent: false,
timeSeries: [],
@@ -273,6 +272,9 @@ export default {
:line-style="path.lineStyle"
:line-color="path.lineColor"
:area-color="path.areaColor"
+ :current-coordinates="currentCoordinates[index]"
+ :current-time-series-index="index"
+ :show-dot="showFlagContent"
/>
<graph-deployment
:deployment-data="reducedDeploymentData"
@@ -298,9 +300,9 @@ export default {
:show-flag-content="showFlagContent"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
- :current-data-index="currentDataIndex"
:legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData"
+ :current-coordinates="currentCoordinates"
/>
</div>
<graph-legend
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index b8202e25685..8a771107de8 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -47,14 +47,14 @@ export default {
type: String,
required: true,
},
- currentDataIndex: {
- type: Number,
- required: true,
- },
legendTitle: {
type: String,
required: true,
},
+ currentCoordinates: {
+ type: Array,
+ required: true,
+ },
},
computed: {
formatTime() {
@@ -90,10 +90,12 @@ export default {
},
},
methods: {
- seriesMetricValue(series) {
+ seriesMetricValue(seriesIndex, series) {
+ const indexFromCoordinates = this.currentCoordinates[seriesIndex]
+ ? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex
- : this.currentDataIndex;
+ : indexFromCoordinates;
const value = series.values[index] && series.values[index].value;
if (isNaN(value)) {
return '-';
@@ -128,7 +130,7 @@ export default {
<h5 v-if="deploymentFlagData">
Deployed
</h5>
- {{ formatDate }} at
+ {{ formatDate }}
<strong>{{ formatTime }}</strong>
</div>
<div
@@ -163,9 +165,11 @@ export default {
:key="index"
>
<track-line :track="series"/>
- <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
<td>
- <strong>{{ seriesMetricValue(series) }}</strong>
+ {{ series.track }} {{ seriesMetricLabel(index, series) }}
+ </td>
+ <td>
+ <strong>{{ seriesMetricValue(index, series) }}</strong>
</td>
</tr>
</table>
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 881560124a5..52f8aa2ee3f 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -22,6 +22,15 @@ export default {
type: String,
required: true,
},
+ currentCoordinates: {
+ type: Object,
+ required: false,
+ default: () => ({ currentX: 0, currentY: 0 }),
+ },
+ showDot: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
strokeDashArray() {
@@ -33,12 +42,20 @@ export default {
};
</script>
<template>
- <g>
+ <g transform="translate(-5, 20)">
+ <circle
+ class="circle-path"
+ :cx="currentCoordinates.currentX"
+ :cy="currentCoordinates.currentY"
+ :fill="lineColor"
+ :stroke="lineColor"
+ r="3"
+ v-if="showDot"
+ />
<path
class="metric-area"
:d="generatedAreaPath"
:fill="areaColor"
- transform="translate(-5, 20)"
/>
<path
class="metric-line"
@@ -47,7 +64,6 @@ export default {
fill="none"
stroke-width="1"
:stroke-dasharray="strokeDashArray"
- transform="translate(-5, 20)"
/>
</g>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
index 79b322e2e42..18be65fd1ef 100644
--- a/app/assets/javascripts/monitoring/components/graph/track_line.vue
+++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue
@@ -19,16 +19,16 @@ export default {
<template>
<td>
<svg
- width="15"
- height="6">
+ width="16"
+ height="8">
<line
:stroke-dasharray="stylizedLine"
:stroke="track.lineColor"
stroke-width="4"
:x1="0"
- :x2="15"
- :y1="2"
- :y2="2"
+ :x2="16"
+ :y1="4"
+ :y2="4"
/>
</svg>
</td>
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index a6dbe42a8f0..241627f9790 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -17,12 +17,12 @@ export default {
<template>
<div
v-if="showPanels"
- class="panel panel-default prometheus-panel"
+ class="card prometheus-panel"
>
- <div class="panel-heading">
+ <div class="card-header">
<h4>{{ name }}</h4>
</div>
- <div class="panel-body prometheus-graph-group">
+ <div class="card-body prometheus-graph-group">
<slot></slot>
</div>
</div>
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 6cc67ba57ee..4f23814ff3e 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -52,14 +52,22 @@ const mixins = {
positionFlag() {
const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
+
this.currentData = timeSeries.values[hoveredDataIndex];
- this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
- if (this.currentXCoordinate > (this.graphWidth - 200)) {
- this.currentFlagPosition = this.currentXCoordinate - 103;
- } else {
- this.currentFlagPosition = this.currentXCoordinate;
- }
+
+ this.currentCoordinates = this.timeSeries.map((series) => {
+ const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
+ const currentData = series.values[currentDataIndex];
+ const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
+ const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
+
+ return {
+ currentX,
+ currentY,
+ currentDataIndex,
+ };
+ });
if (this.hoverData.currentDeployXPos) {
this.showFlag = false;
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
index f3c9acdd93e..d88c13609dc 100644
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -14,7 +14,7 @@ const d3 = {
timeYear,
};
-export const dateFormat = d3.time('%a, %b %-d');
+export const dateFormat = d3.time('%d %b %Y, ');
export const timeFormat = d3.time('%-I:%M%p');
export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left;
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 8a93c7e6bae..4d3f1f1a7cc 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -123,6 +123,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
+ timeSeriesScaleY,
values: timeSeries.values,
max: maximumValue,
average: accum / timeSeries.values.length,
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 2121907dff0..b2c1a26bbae 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1231,8 +1231,8 @@ export default class Notes {
const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
- targetRow.toggle(showNow);
- notesContent.toggle(showNow);
+ targetRow.toggleClass('hide', !showNow);
+ notesContent.toggleClass('hide', !showNow);
}
if (addForm) {
@@ -1427,7 +1427,7 @@ export default class Notes {
const { discussion_html } = data;
const lines = $(discussion_html).find('.line_holder');
lines.addClass('fade-in');
- $container.find('tbody').prepend(lines);
+ $container.find('.diff-content > table > tbody').prepend(lines);
const fileHolder = $container.find('.file-holder');
$container.find('.line-holder-placeholder').remove();
syntaxHighlight(fileHolder);
@@ -1675,7 +1675,7 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/${_.escape(currentUsername)}">
- <span class="hidden-xs">${_.escape(
+ <span class="d-none d-sm-block">${_.escape(
currentUsername,
)}</span>
<span class="note-headline-light">${_.escape(
@@ -1694,7 +1694,7 @@ export default class Notes {
</li>`,
);
- $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname));
+ $tempNote.find('.d-none.d-sm-block').text(_.escape(currentUserFullname));
$tempNote
.find('.note-headline-light')
.text(`@${_.escape(currentUsername)}`);
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 396a675b4ac..72d7e22fba0 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -99,10 +99,6 @@ export default {
'js-note-target-reopen': !this.isOpen,
};
},
- supportQuickActions() {
- // Disable quick actions support for Epics
- return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
- },
markdownDocsPath() {
return this.getNotesData.markdownDocsPath;
},
@@ -325,7 +321,7 @@ Please check your network connection and try again.`;
<li class="timeline-entry">
<div class="timeline-entry-inner">
<div class="flash-container error-alert timeline-content"></div>
- <div class="timeline-icon hidden-xs hidden-sm">
+ <div class="timeline-icon d-none d-sm-none d-md-block">
<user-avatar-link
v-if="author"
:link-href="author.path"
@@ -359,7 +355,7 @@ Please check your network connection and try again.`;
name="note[note]"
class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea"
- :data-supports-quick-actions="supportQuickActions"
+ data-supports-quick-actions="true"
aria-label="Description"
v-model="note"
ref="textarea"
@@ -373,7 +369,7 @@ js-gfm-input js-autosize markdown-area js-vue-textarea"
</markdown-field>
<div class="note-form-actions">
<div
- class="pull-left btn-group
+ class="float-left btn-group
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
<button
@click.prevent="handleSave()"
@@ -387,6 +383,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
name="button"
type="button"
class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
+ data-display="static"
data-toggle="dropdown"
aria-label="Open comment type dropdown">
<i
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index d492d1cd001..cbe4774a360 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -86,7 +86,7 @@ export default {
v-html="resolveSvg"
></span>
</span>
- <span class=".line-resolve-text">
+ <span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
</span>
</div>
diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index 4ddca918495..2dc39d1a186 100644
--- a/app/assets/javascripts/notes/components/note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
@@ -32,17 +32,17 @@ export default {
<template>
<div :class="className">
{{ actionText }}
- <time-ago-tooltip
- :time="editedAt"
- tooltip-placement="bottom"
- />
<template v-if="editedBy">
- by
+ {{ s__('ByAuthor|by') }}
<a
:href="editedBy.path"
class="js-vue-author author_link">
{{ editedBy.name }}
</a>
</template>
+ <time-ago-tooltip
+ :time="editedAt"
+ tooltip-placement="bottom"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index c3d1ef1fcc6..a4081957207 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -62,6 +62,21 @@ export default {
<template>
<div class="note-header-info">
+ <div
+ v-if="includeToggle"
+ class="discussion-actions">
+ <button
+ @click="handleToggle"
+ class="note-action-button discussion-toggle-button js-vue-toggle-button"
+ type="button">
+ <i
+ :class="toggleChevronClass"
+ class="fa"
+ aria-hidden="true">
+ </i>
+ {{ __('Toggle discussion') }}
+ </button>
+ </div>
<a :href="author.path">
<span class="note-header-author-name">{{ author.name }}</span>
<span class="note-headline-light">
@@ -78,10 +93,13 @@ export default {
v-html="actionTextHtml"
class="system-note-message">
</span>
+ <span class="system-note-separator">
+ &middot;
+ </span>
<a
:href="noteTimestampLink"
@click="updateTargetNoteHash"
- class="note-timestamp">
+ class="note-timestamp system-note-separator">
<time-ago-tooltip
:time="createdAt"
tooltip-placement="bottom"
@@ -95,20 +113,5 @@ export default {
</i>
</span>
</span>
- <div
- v-if="includeToggle"
- class="discussion-actions">
- <button
- @click="handleToggle"
- class="note-action-button discussion-toggle-button js-vue-toggle-button"
- type="button">
- <i
- :class="toggleChevronClass"
- class="fa"
- aria-hidden="true">
- </i>
- Toggle discussion
- </button>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index e0f883a8e08..7f5aacaa3a2 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -100,7 +100,7 @@ export default {
: 'div';
},
wrapperClass() {
- return this.isDiffDiscussion ? '' : 'panel panel-default';
+ return this.isDiffDiscussion ? '' : 'card';
},
},
mounted() {
@@ -263,7 +263,7 @@ Please check your network connection and try again.`;
class="discussion-reply-holder">
<template v-if="!isReplying && canReply">
<div
- class="btn-group-justified discussion-with-resolve-btn"
+ class="btn-group d-flex discussion-with-resolve-btn"
role="group">
<div
class="btn-group"
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 244a6980b5a..98ce070288e 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => {
scrollToElement(el);
}
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index f89591a54d6..787be6f4c99 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length;
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 9e6cf67dff0..00e27ca0e70 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -15,7 +15,7 @@ export default class NotificationsForm {
toggleCheckbox(e) {
const $checkbox = $(e.currentTarget);
- const $parent = $checkbox.closest('.checkbox');
+ const $parent = $checkbox.closest('.form-check');
this.saveEvent($checkbox, $parent);
}
diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js
index 91f154b7ecd..ff4d6ab15f9 100644
--- a/app/assets/javascripts/pages/admin/admin.js
+++ b/app/assets/javascripts/pages/admin/admin.js
@@ -25,7 +25,7 @@ export default function adminInit() {
$('body').on('click', '.js-toggle-colors-link', (e) => {
e.preventDefault();
- $('.js-toggle-colors-container').toggle();
+ $('.js-toggle-colors-container').toggleClass('hide');
});
$('.log-tabs a').on('click', function logTabsClick(e) {
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 0e3ac636661..9ce176744ba 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -52,16 +52,15 @@
text() {
const keepContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
- This will delete all of the issues, merge requests, and groups linked to them.
+ Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
const deleteContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
- Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
+ This will delete all of the issues, merge requests, and groups linked to them.
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
-
return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
{
username: `<strong>${_.escape(this.username)}</strong>`,
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index 04a0d8117cc..d3b2656743d 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -1,6 +1,10 @@
+import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
document.addEventListener('DOMContentLoaded', () => {
+ // Initialize expandable settings panels
+ initSettingsPanels();
+
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
diff --git a/app/assets/javascripts/pages/ide/index.js b/app/assets/javascripts/pages/ide/index.js
new file mode 100644
index 00000000000..efadf6967aa
--- /dev/null
+++ b/app/assets/javascripts/pages/ide/index.js
@@ -0,0 +1,9 @@
+import { initIde, resetServiceWorkersPublicPath } from '~/ide/index';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const ideElement = document.getElementById('ide');
+ if (ideElement) {
+ resetServiceWorkersPublicPath();
+ initIde(ideElement);
+ }
+});
diff --git a/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js
new file mode 100644
index 00000000000..edd7e38471b
--- /dev/null
+++ b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js
@@ -0,0 +1,3 @@
+import initU2F from '../../../shared/sessions/u2f';
+
+document.addEventListener('DOMContentLoaded', initU2F);
diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index fbdef329ab2..8e8f47c21d8 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -5,7 +5,7 @@ document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true';
if (skippable) {
- const button = `<a class="btn btn-xs btn-warning pull-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
+ const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
const flashAlert = document.querySelector('.flash-alert .container-fluid');
if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button);
}
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
new file mode 100644
index 00000000000..0c2d7d7c96a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
@@ -0,0 +1,3 @@
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
+gcpSignupOffer();
diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js
new file mode 100644
index 00000000000..0c2d7d7c96a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/new/index.js
@@ -0,0 +1,3 @@
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
+gcpSignupOffer();
diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js
index d1c78bd61db..768da8fb236 100644
--- a/app/assets/javascripts/pages/projects/compare/index.js
+++ b/app/assets/javascripts/pages/projects/compare/index.js
@@ -1,3 +1,3 @@
import initCompareAutocomplete from '~/compare_autocomplete';
-document.addEventListener('DOMContentLoaded', initCompareAutocomplete);
+document.addEventListener('DOMContentLoaded', () => initCompareAutocomplete());
diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js
index 2b4fd3c47c0..a626ed2d30b 100644
--- a/app/assets/javascripts/pages/projects/compare/show/index.js
+++ b/app/assets/javascripts/pages/projects/compare/show/index.js
@@ -1,8 +1,10 @@
import Diff from '~/diff';
import initChangesDropdown from '~/init_changes_dropdown';
+import GpgBadges from '~/gpg_badges';
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new
const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
+ GpgBadges.fetch();
});
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
index a99ce0f1c36..5316d3e9f3c 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
+/* eslint-disable func-names, space-before-function-paren, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
import $ from 'jquery';
import _ from 'underscore';
@@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats';
const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
-const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
const hasProp = {}.hasOwnProperty;
+const extend = function(child, parent) { for (const 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; };
export const ContributorsGraph = (function() {
function ContributorsGraph() {}
ContributorsGraph.prototype.MARGIN = {
top: 20,
- right: 20,
+ right: 10,
bottom: 30,
- left: 50
+ left: 40
};
ContributorsGraph.prototype.x_domain = null;
@@ -32,6 +32,12 @@ export const ContributorsGraph = (function() {
ContributorsGraph.prototype.dates = [];
+ ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) {
+ const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
+ const marginWidth = this.MARGIN.left + this.MARGIN.right;
+ return baseWidth - parentPaddingWidth - marginWidth;
+ };
+
ContributorsGraph.set_x_domain = function(data) {
return ContributorsGraph.prototype.x_domain = data;
};
@@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) {
function ContributorsMasterGraph(data1) {
const $parentElement = $('#contributors-master');
- const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
this.data = data1;
this.update_content = this.update_content.bind(this);
- this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right);
+ this.width = this.determine_width($('.js-graphs-show').width(), $parentElement);
this.height = 200;
this.x = null;
this.y = null;
@@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) {
}
ContributorsMasterGraph.prototype.process_dates = function(data) {
- var dates;
- dates = this.get_dates(data);
+ const dates = this.get_dates(data);
this.parse_dates(data);
return ContributorsGraph.set_dates(dates);
};
@@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.parse_dates = function(data) {
- var parseDate;
- parseDate = d3.timeParse("%Y-%m-%d");
+ const parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) {
return d.date = parseDate(d.date);
});
@@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_svg = function() {
- return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ this.svg = d3.select("#contributors-master")
+ .append("svg")
+ .attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
+ .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
+ .attr("class", "tint-box")
+ .append("g")
+ .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ return this.svg;
};
ContributorsMasterGraph.prototype.create_area = function(x, y) {
@@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) {
extend(ContributorsAuthorGraph, superClass);
function ContributorsAuthorGraph(data1) {
+ const $parentElements = $('.person');
+
this.data = data1;
// Don't split graph size in half for mobile devices.
- if ($(window).width() < 768) {
- this.width = $('.content').width() - 80;
+ if ($(window).width() < 790) {
+ this.width = this.determine_width($('.js-graphs-show').width(), $parentElements);
} else {
- this.width = ($('.content').width() / 2) - 100;
+ this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements);
}
this.height = 200;
this.x = null;
@@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) {
ContributorsAuthorGraph.prototype.create_area = function(x, y) {
return this.area = d3.area().x(function(d) {
- var parseDate;
- parseDate = d3.timeParse("%Y-%m-%d");
+ const parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d));
}).y0(this.height).y1((function(_this) {
return function(d) {
@@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) {
};
ContributorsAuthorGraph.prototype.create_svg = function() {
- var persons = document.querySelectorAll('.person');
+ const persons = document.querySelectorAll('.person');
this.list_item = persons[persons.length - 1];
- return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ this.svg = d3.select(this.list_item)
+ .append("svg")
+ .attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
+ .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
+ .attr("class", "spark")
+ .append("g")
+ .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ return this.svg;
};
ContributorsAuthorGraph.prototype.draw_path = function(data) {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
new file mode 100644
index 00000000000..46f3f55a400
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
@@ -0,0 +1,60 @@
+import $ from 'jquery';
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
+import axios from '~/lib/utils/axios_utils';
+import initCompareAutocomplete from '~/compare_autocomplete';
+import initTargetProjectDropdown from './target_project_dropdown';
+
+const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
+ $loadingIndicator.show();
+ $commitList.empty();
+
+ return axios
+ .get(url, {
+ params,
+ })
+ .then(({ data }) => {
+ $loadingIndicator.hide();
+ $commitList.html(data);
+ localTimeAgo($('.js-timeago', $commitList));
+ });
+};
+
+export default mrNewCompareNode => {
+ const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset;
+ initTargetProjectDropdown();
+
+ const updateSourceBranchCommitList = () =>
+ updateCommitList(
+ sourceBranchUrl,
+ $(mrNewCompareNode).find('.js-source-loading'),
+ $(mrNewCompareNode).find('.mr_source_commit'),
+ {
+ ref: $(mrNewCompareNode)
+ .find("input[name='merge_request[source_branch]']")
+ .val(),
+ },
+ );
+ const updateTargetBranchCommitList = () =>
+ updateCommitList(
+ targetBranchUrl,
+ $(mrNewCompareNode).find('.js-target-loading'),
+ $(mrNewCompareNode).find('.mr_target_commit'),
+ {
+ target_project_id: $(mrNewCompareNode)
+ .find("input[name='merge_request[target_project_id]']")
+ .val(),
+ ref: $(mrNewCompareNode)
+ .find("input[name='merge_request[target_branch]']")
+ .val(),
+ },
+ );
+ initCompareAutocomplete('branches', $dropdown => {
+ if ($dropdown.is('.js-target-branch')) {
+ updateTargetBranchCommitList();
+ } else if ($dropdown.is('.js-source-branch')) {
+ updateSourceBranchCommitList();
+ }
+ });
+ updateSourceBranchCommitList();
+ updateTargetBranchCommitList();
+};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index 6c9afddefac..01a0b4870c1 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -1,18 +1,15 @@
-import Compare from '~/compare';
import MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import initCompare from './compare';
document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
- new Compare({ // eslint-disable-line no-new
- targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
- sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
- targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
- });
+ initCompare(mrNewCompareNode);
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
- new MergeRequest({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new MergeRequest({
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
initPipelines();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
new file mode 100644
index 00000000000..b72fe6681df
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
@@ -0,0 +1,22 @@
+import $ from 'jquery';
+
+export default () => {
+ const $targetProjectDropdown = $('.js-target-project');
+ $targetProjectDropdown.glDropdown({
+ selectable: true,
+ fieldName: $targetProjectDropdown.data('fieldName'),
+ filterable: true,
+ id(obj, $el) {
+ return $el.data('id');
+ },
+ toggleLabel(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked({ $el }) {
+ $('.mr_target_commit').empty();
+ const $targetBranchDropdown = $('.js-target-branch');
+ $targetBranchDropdown.data('refsUrl', $el.data('refsUrl'));
+ $targetBranchDropdown.data('glDropdown').clearMenu();
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
index 9aa8945e268..b0b077a5e4c 100644
--- a/app/assets/javascripts/pages/projects/pipelines/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -1,6 +1,12 @@
import $ from 'jquery';
import NewBranchForm from '~/new_branch_form';
+import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
document.addEventListener('DOMContentLoaded', () => {
new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
+
+ setupNativeFormVariableList({
+ container: $('.js-ci-variable-list-section'),
+ formField: 'variables_attributes',
+ });
});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
index 25a88f846eb..17b91479ea5 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue
@@ -42,7 +42,7 @@
</label>
<span
v-if="helpText"
- class="help-block"
+ class="form-text text-muted"
>
{{ helpText }}
</span>
diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
new file mode 100644
index 00000000000..df21e2f8771
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -0,0 +1,77 @@
+<script>
+import _ from 'underscore';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ deleteWikiUrl: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ pageTitle: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ csrfToken: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ },
+ computed: {
+ message() {
+ return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?');
+ },
+ title() {
+ return sprintf(
+ s__('WikiPageConfirmDelete|Delete page %{pageTitle}?'),
+ {
+ pageTitle: _.escape(this.pageTitle),
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ id="delete-wiki-modal"
+ :header-title-text="title"
+ footer-primary-button-variant="danger"
+ :footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')"
+ @submit="onSubmit"
+ >
+ {{ message }}
+ <form
+ ref="form"
+ :action="deleteWikiUrl"
+ method="post"
+ class="form-horizontal js-requires-input"
+ >
+ <input
+ ref="method"
+ type="hidden"
+ name="_method"
+ value="delete"
+ />
+ <input
+ type="hidden"
+ name="authenticity_token"
+ :value="csrfToken"
+ />
+ </form>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index ec01c66ffda..0295653cb29 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -1,12 +1,40 @@
import $ from 'jquery';
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import csrf from '~/lib/utils/csrf';
import Wikis from './wikis';
import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
+import deleteWikiModal from './components/delete_wiki_modal.vue';
document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
+
+ const deleteWikiButton = document.getElementById('delete-wiki-button');
+
+ if (deleteWikiButton) {
+ Vue.use(Translate);
+
+ const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset;
+ const deleteWikiModalEl = document.getElementById('delete-wiki-modal');
+ const deleteModal = new Vue({ // eslint-disable-line
+ el: deleteWikiModalEl,
+ data: {
+ deleteWikiUrl: '',
+ },
+ render(createElement) {
+ return createElement(deleteWikiModal, {
+ props: {
+ pageTitle,
+ deleteWikiUrl,
+ csrfToken: csrf.token,
+ },
+ });
+ },
+ });
+ }
});
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 8ce938c958b..50d042fef29 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
- date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes);
+ date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
return date;
}
@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`;
}
-const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+const initColorKey = () =>
+ d3
+ .scaleLinear()
+ .range(['#acd5f2', '#254e77'])
+ .domain([0, 3]);
export default class ActivityCalendar {
- constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
+ constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
this.daySize = 15;
- this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
- this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ this.daySizeWithSpace = this.daySize + this.daySpace * 2;
+ this.monthNames = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec',
+ ];
this.months = [];
+ this.firstDayOfWeek = firstDayOfWeek;
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
@@ -70,7 +88,7 @@ export default class ActivityCalendar {
// Create a new group array if this is the first day of the week
// or if is first object
- if ((day === 0 && i !== 0) || i === 0) {
+ if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
group += 1;
}
@@ -109,21 +127,30 @@ export default class ActivityCalendar {
}
renderSvg(container, group) {
- const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group);
- return d3.select(container)
+ const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
+ return d3
+ .select(container)
.append('svg')
- .attr('width', width)
- .attr('height', 167)
- .attr('class', 'contrib-calendar');
+ .attr('width', width)
+ .attr('height', 167)
+ .attr('class', 'contrib-calendar');
+ }
+
+ dayYPos(day) {
+ return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
}
renderDays() {
- this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g')
+ this.svg
+ .selectAll('g')
+ .data(this.timestampsTmp)
+ .enter()
+ .append('g')
.attr('transform', (group, i) => {
_.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) {
const month = stamp.date.getMonth();
- const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace;
+ const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months);
if (
lastMonth == null ||
@@ -133,86 +160,113 @@ export default class ActivityCalendar {
}
}
});
- return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`;
+ return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
})
.selectAll('rect')
- .data(stamp => stamp)
- .enter()
- .append('rect')
- .attr('x', '0')
- .attr('y', stamp => this.daySizeWithSpace * stamp.day)
- .attr('width', this.daySize)
- .attr('height', this.daySize)
- .attr('fill', stamp => (
- stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'
- ))
- .attr('title', stamp => formatTooltipText(stamp))
- .attr('class', 'user-contrib-cell js-tooltip')
- .attr('data-container', 'body')
- .on('click', this.clickDay);
+ .data(stamp => stamp)
+ .enter()
+ .append('rect')
+ .attr('x', '0')
+ .attr('y', stamp => this.dayYPos(stamp.day))
+ .attr('width', this.daySize)
+ .attr('height', this.daySize)
+ .attr(
+ 'fill',
+ stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'),
+ )
+ .attr('title', stamp => formatTooltipText(stamp))
+ .attr('class', 'user-contrib-cell js-tooltip')
+ .attr('data-container', 'body')
+ .on('click', this.clickDay);
}
renderDayTitles() {
const days = [
{
text: 'M',
- y: 29 + (this.daySizeWithSpace * 1),
- }, {
+ y: 29 + this.dayYPos(1),
+ },
+ {
text: 'W',
- y: 29 + (this.daySizeWithSpace * 3),
- }, {
+ y: 29 + this.dayYPos(3),
+ },
+ {
text: 'F',
- y: 29 + (this.daySizeWithSpace * 5),
+ y: 29 + this.dayYPos(5),
},
];
- this.svg.append('g')
+ this.svg
+ .append('g')
.selectAll('text')
- .data(days)
- .enter()
- .append('text')
- .attr('text-anchor', 'middle')
- .attr('x', 8)
- .attr('y', day => day.y)
- .text(day => day.text)
- .attr('class', 'user-contrib-text');
+ .data(days)
+ .enter()
+ .append('text')
+ .attr('text-anchor', 'middle')
+ .attr('x', 8)
+ .attr('y', day => day.y)
+ .text(day => day.text)
+ .attr('class', 'user-contrib-text');
}
renderMonths() {
- this.svg.append('g')
+ this.svg
+ .append('g')
.attr('direction', 'ltr')
.selectAll('text')
- .data(this.months)
- .enter()
- .append('text')
- .attr('x', date => date.x)
- .attr('y', 10)
- .attr('class', 'user-contrib-text')
- .text(date => this.monthNames[date.month]);
+ .data(this.months)
+ .enter()
+ .append('text')
+ .attr('x', date => date.x)
+ .attr('y', 10)
+ .attr('class', 'user-contrib-text')
+ .text(date => this.monthNames[date.month]);
}
renderKey() {
- const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions'];
- const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
+ const keyValues = [
+ 'no contributions',
+ '1-9 contributions',
+ '10-19 contributions',
+ '20-29 contributions',
+ '30+ contributions',
+ ];
+ const keyColors = [
+ '#ededed',
+ this.colorKey(0),
+ this.colorKey(1),
+ this.colorKey(2),
+ this.colorKey(3),
+ ];
- this.svg.append('g')
- .attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`)
+ this.svg
+ .append('g')
+ .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect')
- .data(keyColors)
- .enter()
- .append('rect')
- .attr('width', this.daySize)
- .attr('height', this.daySize)
- .attr('x', (color, i) => this.daySizeWithSpace * i)
- .attr('y', 0)
- .attr('fill', color => color)
- .attr('class', 'js-tooltip')
- .attr('title', (color, i) => keyValues[i])
- .attr('data-container', 'body');
+ .data(keyColors)
+ .enter()
+ .append('rect')
+ .attr('width', this.daySize)
+ .attr('height', this.daySize)
+ .attr('x', (color, i) => this.daySizeWithSpace * i)
+ .attr('y', 0)
+ .attr('fill', color => color)
+ .attr('class', 'js-tooltip')
+ .attr('title', (color, i) => keyValues[i])
+ .attr('data-container', 'body');
}
initColor() {
- const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
- return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
+ const colorRange = [
+ '#ededed',
+ this.colorKey(0),
+ this.colorKey(1),
+ this.colorKey(2),
+ this.colorKey(3),
+ ];
+ return d3
+ .scaleThreshold()
+ .domain([0, 10, 20, 30])
+ .range(colorRange);
}
clickDay(stamp) {
@@ -227,14 +281,15 @@ export default class ActivityCalendar {
$('.user-calendar-activities').html(LOADING_HTML);
- axios.get(this.calendarActivitiesPath, {
- params: {
- date,
- },
- responseType: 'text',
- })
- .then(({ data }) => $('.user-calendar-activities').html(data))
- .catch(() => flash(__('An error occurred while retrieving calendar activity')));
+ axios
+ .get(this.calendarActivitiesPath, {
+ params: {
+ date,
+ },
+ responseType: 'text',
+ })
+ .then(({ data }) => $('.user-calendar-activities').html(data))
+ .catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else {
this.currentSelectedDate = '';
$('.user-calendar-activities').html('');
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 124bc2ba710..ca375007ec5 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -181,7 +181,7 @@ export default class UserTabs {
toggleLoading(status) {
return this.$parentEl.find('.loading-status .loading')
- .toggle(status);
+ .toggleClass('hidden', status);
}
setCurrentAction(source) {
@@ -195,6 +195,6 @@ export default class UserTabs {
}
getCurrentAction() {
- return this.$parentEl.find('.nav-links .active a').data('action');
+ return this.$parentEl.find('.nav-links a.active').data('action');
}
}
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 2fd1715ee79..8ffaa52d9e8 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -5,7 +5,6 @@ import PerformanceBarService from '../services/performance_bar_service';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
-import upstreamPerformanceBar from './upstream_performance_bar.vue';
import Flash from '../../flash';
@@ -14,7 +13,6 @@ export default {
detailedMetric,
requestSelector,
simpleMetric,
- upstreamPerformanceBar,
},
props: {
store: {
@@ -128,9 +126,6 @@ export default {
{{ currentRequest.details.host.hostname }}
</span>
</div>
- <upstream-performance-bar
- v-if="initialRequest && currentRequest.details"
- />
<detailed-metric
v-for="metric in $options.detailedMetrics"
:key="metric.metric"
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index 3ed07a4a47d..dd9578a6c7f 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -37,7 +37,7 @@ export default {
<template>
<div
id="peek-request-selector"
- class="pull-right"
+ class="float-right"
>
<select v-model="currentRequestId">
<option
diff --git a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue
deleted file mode 100644
index 2b5915f381f..00000000000
--- a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<script>
-export default {
- mounted() {
- const upstreamPerformanceBar = document
- .getElementById('peek-view-performance-bar')
- .cloneNode(true);
-
- upstreamPerformanceBar.classList.remove('hidden');
-
- this.$refs.wrapper.appendChild(upstreamPerformanceBar);
- },
-};
-</script>
-<template>
- <div
- id="peek-view-performance-bar-vue"
- class="view"
- ref="wrapper"
- ></div>
-</template>
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index a0ddf36a672..4a98aed7679 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -1,5 +1,3 @@
-import 'vendor/peek.performance_bar';
-
import Vue from 'vue';
import performanceBarApp from './components/performance_bar_app.vue';
import PerformanceBarStore from './stores/performance_bar_store';
diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue
deleted file mode 100644
index 0cdffbde05b..00000000000
--- a/app/assets/javascripts/pipelines/components/async_button.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script>
- /* eslint-disable no-alert */
-
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
-
- export default {
- directives: {
- tooltip,
- },
- components: {
- loadingIcon,
- icon,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- icon: {
- type: String,
- required: true,
- },
- cssClass: {
- type: String,
- required: true,
- },
- pipelineId: {
- type: Number,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- isLoading: false,
- };
- },
- computed: {
- buttonClass() {
- return `btn ${this.cssClass}`;
- },
- },
- created() {
- // We're using eventHub to listen to the modal here instead of
- // using props because it would would make the parent components
- // much more complex to keep track of the loading state of each button
- eventHub.$on('postAction', this.setLoading);
- },
- beforeDestroy() {
- eventHub.$off('postAction', this.setLoading);
- },
- methods: {
- onClick() {
- eventHub.$emit('openConfirmationModal', {
- pipelineId: this.pipelineId,
- endpoint: this.endpoint,
- type: this.type,
- });
- },
- setLoading(endpoint) {
- if (endpoint === this.endpoint) {
- this.isLoading = true;
- }
- },
- },
- };
-</script>
-
-<template>
- <button
- v-tooltip
- type="button"
- @click="onClick"
- :class="buttonClass"
- :title="title"
- :aria-label="title"
- data-container="body"
- data-placement="top"
- :disabled="isLoading">
- <icon
- :name="icon"
- />
- <loading-icon v-if="isLoading" />
- </button>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue
index 8d3d6223d7b..f3219b8291c 100644
--- a/app/assets/javascripts/pipelines/components/blank_state.vue
+++ b/app/assets/javascripts/pipelines/components/blank_state.vue
@@ -17,13 +17,13 @@
<template>
<div class="row empty-state">
- <div class="col-xs-12">
+ <div class="col-12">
<div class="svg-content">
<img :src="svgPath" />
</div>
</div>
- <div class="col-xs-12 text-center">
+ <div class="col-12 text-center">
<div class="text-content">
<h4>{{ message }}</h4>
</div>
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index 10ac8c08bed..50c27bed9fd 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -19,13 +19,13 @@
</script>
<template>
<div class="row empty-state js-empty-state">
- <div class="col-xs-12">
+ <div class="col-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
</div>
</div>
- <div class="col-xs-12">
+ <div class="col-12">
<div class="text-content">
<template v-if="canSetCi">
@@ -34,7 +34,7 @@
</h4>
<p>
- {{ s__(`Pipelines|Continous Integration can help
+ {{ s__(`Pipelines|Continuous Integration can help
catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver
code to your product environment.`) }}
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index e99d949801f..82b4ce083fb 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,11 +1,21 @@
<script>
import $ from 'jquery';
-import tooltip from '../../../vue_shared/directives/tooltip';
-import Icon from '../../../vue_shared/components/icon.vue';
-import { dasherize } from '../../../lib/utils/text_utility';
-import eventHub from '../../event_hub';
+import axios from '~/lib/utils/axios_utils';
+import { dasherize } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+
/**
- * Renders either a cancel, retry or play icon pointing to the given path.
+ * Renders either a cancel, retry or play icon button and handles the post request
+ *
+ * Used in:
+ * - mr widget mini pipeline graph: `mr_widget_pipeline.vue`
+ * - pipelines table
+ * - pipelines table in merge request page
+ * - pipelines table in commit page
+ * - pipelines detail page in big graph
*/
export default {
components: {
@@ -32,26 +42,41 @@ export default {
required: true,
},
- buttonDisabled: {
- type: String,
- required: false,
- default: null,
- },
},
+ data() {
+ return {
+ isDisabled: false,
+ };
+ },
+
computed: {
cssClass() {
const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
- isDisabled() {
- return this.buttonDisabled === this.link;
- },
},
-
methods: {
+ /**
+ * The request should not be handled here.
+ * However due to this component being used in several
+ * different apps it avoids repetition & complexity.
+ *
+ */
onClickAction() {
$(this.$el).tooltip('hide');
- eventHub.$emit('graphAction', this.link);
+
+ this.isDisabled = true;
+
+ axios.post(`${this.link}.json`)
+ .then(() => {
+ this.isDisabled = false;
+ this.$emit('pipelineActionRequestComplete');
+ })
+ .catch(() => {
+ this.isDisabled = false;
+
+ createFlash(__('An error occurred while making the request.'));
+ });
},
},
};
@@ -62,11 +87,12 @@ export default {
@click="onClickAction"
v-tooltip
:title="tooltipText"
- class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
+ class="js-ci-action btn btn-blank
+btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass"
data-container="body"
:disabled="isDisabled"
>
- <icon :name="actionIcon" />
+ <icon :name="actionIcon"/>
</button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
deleted file mode 100644
index 7c4fd65e36f..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import tooltip from '../../../vue_shared/directives/tooltip';
-
- /**
- * Renders either a cancel, retry or play icon pointing to the given path.
- * TODO: Remove UJS from here and use an async request instead.
- */
- export default {
- components: {
- icon,
- },
-
- directives: {
- tooltip,
- },
- props: {
- tooltipText: {
- type: String,
- required: true,
- },
-
- link: {
- type: String,
- required: true,
- },
-
- actionMethod: {
- type: String,
- required: true,
- },
-
- actionIcon: {
- type: String,
- required: true,
- },
- },
- };
-</script>
-<template>
- <a
- v-tooltip
- :data-method="actionMethod"
- :title="tooltipText"
- :href="link"
- rel="nofollow"
- class="ci-action-icon-wrapper js-ci-status-icon"
- data-container="body"
- aria-label="Job's action"
- >
- <icon :name="actionIcon" />
- </a>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index be213c2ee78..7bfe11ab8cd 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -1,87 +1,93 @@
<script>
- import $ from 'jquery';
- import jobNameComponent from './job_name_component.vue';
- import jobComponent from './job_component.vue';
- import tooltip from '../../../vue_shared/directives/tooltip';
+import $ from 'jquery';
+import JobNameComponent from './job_name_component.vue';
+import JobComponent from './job_component.vue';
+import tooltip from '../../../vue_shared/directives/tooltip';
- /**
- * Renders the dropdown for the pipeline graph.
- *
- * The following object should be provided as `job`:
- *
- * {
- * "id": 4256,
- * "name": "test",
- * "status": {
- * "icon": "icon_status_success",
- * "text": "passed",
- * "label": "passed",
- * "group": "success",
- * "details_path": "/root/ci-mock/builds/4256",
- * "action": {
- * "icon": "retry",
- * "title": "Retry",
- * "path": "/root/ci-mock/builds/4256/retry",
- * "method": "post"
- * }
- * }
- * }
- */
- export default {
- directives: {
- tooltip,
- },
+/**
+ * Renders the dropdown for the pipeline graph.
+ *
+ * The following object should be provided as `job`:
+ *
+ * {
+ * "id": 4256,
+ * "name": "test",
+ * "status": {
+ * "icon": "icon_status_success",
+ * "text": "passed",
+ * "label": "passed",
+ * "group": "success",
+ * "details_path": "/root/ci-mock/builds/4256",
+ * "action": {
+ * "icon": "retry",
+ * "title": "Retry",
+ * "path": "/root/ci-mock/builds/4256/retry",
+ * "method": "post"
+ * }
+ * }
+ * }
+ */
+export default {
+ directives: {
+ tooltip,
+ },
- components: {
- jobComponent,
- jobNameComponent,
- },
+ components: {
+ JobComponent,
+ JobNameComponent,
+ },
- props: {
- job: {
- type: Object,
- required: true,
- },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
+ },
- computed: {
- tooltipText() {
- return `${this.job.name} - ${this.job.status.label}`;
- },
+ computed: {
+ tooltipText() {
+ return `${this.job.name} - ${this.job.status.label}`;
},
+ },
- mounted() {
- this.stopDropdownClickPropagation();
- },
+ mounted() {
+ this.stopDropdownClickPropagation();
+ },
- methods: {
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
+ methods: {
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name or the action icon
+ * the dropdown should not be closed so we stop propagation
+ * of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
- stopDropdownClickPropagation() {
- $(this.$el
- .querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
- .on('click', (e) => {
- e.stopPropagation();
- });
- },
+ stopDropdownClickPropagation() {
+ $(
+ '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
+ this.$el,
+ ).on('click', e => {
+ e.stopPropagation();
+ });
+ },
+
+ pipelineActionRequestComplete() {
+ this.$emit('pipelineActionRequestComplete');
},
- };
+ },
+};
</script>
<template>
- <div class="ci-job-dropdown-container">
+ <div class="ci-job-dropdown-container dropdown">
<button
v-tooltip
type="button"
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
- :title="tooltipText">
+ :title="tooltipText"
+ >
<job-name-component
:name="job.name"
@@ -98,11 +104,12 @@
<ul>
<li
v-for="(item, i) in job.jobs"
- :key="i">
+ :key="i"
+ >
<job-component
:job="item"
- :is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ac9ce7e47d6..4ec67f6c01b 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -7,7 +7,6 @@ export default {
StageColumnComponent,
LoadingIcon,
},
-
props: {
isLoading: {
type: Boolean,
@@ -17,11 +16,6 @@ export default {
type: Object,
required: true,
},
- actionDisabled: {
- type: String,
- required: false,
- default: null,
- },
},
computed: {
@@ -52,6 +46,10 @@ export default {
return className;
},
+
+ refreshPipelineGraph() {
+ this.$emit('refreshPipelineGraph');
+ },
},
};
</script>
@@ -75,7 +73,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
- :action-disabled="actionDisabled"
+ @refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index c6e5ae6df41..dc16d395bcb 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -1,6 +1,5 @@
<script>
import ActionComponent from './action_component.vue';
-import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default {
components: {
ActionComponent,
- DropdownActionComponent,
JobNameComponent,
},
-
directives: {
tooltip,
},
@@ -44,26 +41,12 @@ export default {
type: Object,
required: true,
},
-
cssClassJobName: {
type: String,
required: false,
default: '',
},
-
- isDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- actionDisabled: {
- type: String,
- required: false,
- default: null,
- },
},
-
computed: {
status() {
return this.job && this.job.status ? this.job.status : {};
@@ -96,6 +79,11 @@ export default {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
},
+ methods: {
+ pipelineActionRequestComplete() {
+ this.$emit('pipelineActionRequestComplete');
+ },
+ },
};
</script>
<template>
@@ -108,6 +96,7 @@ export default {
:class="cssClassJobName"
data-container="body"
data-html="true"
+ data-boundary="viewport"
class="js-pipeline-graph-job-link"
>
@@ -120,7 +109,7 @@ export default {
<div
v-else
v-tooltip
- class="js-job-component-tooltip"
+ class="js-job-component-tooltip non-details-job-component"
:title="tooltipText"
:class="cssClassJobName"
data-html="true"
@@ -134,19 +123,11 @@ export default {
</div>
<action-component
- v-if="hasAction && !isDropdown"
- :tooltip-text="status.action.title"
- :link="status.action.path"
- :action-icon="status.action.icon"
- :button-disabled="actionDisabled"
- />
-
- <dropdown-action-component
- v-if="hasAction && isDropdown"
+ v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
- :action-method="status.action.method"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index f6e6569e15b..f32368947e8 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -29,11 +29,6 @@ export default {
required: false,
default: '',
},
- actionDisabled: {
- type: String,
- required: false,
- default: null,
- },
},
methods: {
@@ -48,6 +43,10 @@ export default {
buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
+
+ pipelineActionRequestComplete() {
+ this.$emit('refreshPipelineGraph');
+ },
},
};
</script>
@@ -74,12 +73,13 @@ export default {
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
- :action-disabled="actionDisabled"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index ceb4d9ca604..4d965733f95 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -46,7 +46,7 @@
};
</script>
<template>
- <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags">
+ <div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags">
<a
:href="pipeline.path"
class="js-pipeline-url-link">
@@ -69,35 +69,35 @@
<span
v-if="pipeline.flags.latest"
v-tooltip
- class="js-pipeline-url-latest label label-success"
+ class="js-pipeline-url-latest badge badge-success"
title="Latest pipeline for this branch">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
v-tooltip
- class="js-pipeline-url-yaml label label-danger"
+ class="js-pipeline-url-yaml badge badge-danger"
:title="pipeline.yaml_errors">
yaml invalid
</span>
<span
v-if="pipeline.flags.failure_reason"
v-tooltip
- class="js-pipeline-url-failure label label-danger"
+ class="js-pipeline-url-failure badge badge-danger"
:title="pipeline.failure_reason">
error
</span>
<a
v-if="pipeline.flags.auto_devops"
tabindex="0"
- class="js-pipeline-url-autodevops label label-info autodevops-badge"
+ class="js-pipeline-url-autodevops badge badge-info autodevops-badge"
v-popover="popoverOptions"
role="button">
Auto DevOps
</a>
<span
v-if="pipeline.flags.stuck"
- class="js-pipeline-url-stuck label label-warning">
+ class="js-pipeline-url-stuck badge badge-warning">
stuck
</span>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 3297af7bde4..e9bc3cf14ca 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -63,7 +63,7 @@
<loading-icon v-if="isLoading" />
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(action, i) in actions"
:key="i"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index 1b9e0f917a4..31fcc9dd412 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -37,7 +37,7 @@
>
</i>
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(artifact, i) in artifacts"
:key="i">
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 714aed1333e..4318abe97e0 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -1,7 +1,7 @@
<script>
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+ import Modal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
- import pipelinesTableRowComponent from './pipelines_table_row.vue';
+ import PipelinesTableRowComponent from './pipelines_table_row.vue';
import eventHub from '../event_hub';
/**
@@ -11,8 +11,8 @@
*/
export default {
components: {
- pipelinesTableRowComponent,
- DeprecatedModal,
+ PipelinesTableRowComponent,
+ Modal,
},
props: {
pipelines: {
@@ -37,30 +37,19 @@
return {
pipelineId: '',
endpoint: '',
- type: '',
+ cancelingPipeline: null,
};
},
computed: {
modalTitle() {
- return this.type === 'stop' ?
- sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
- pipelineId: `'${this.pipelineId}'`,
- }, false) :
- sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), {
- pipelineId: `'${this.pipelineId}'`,
- }, false);
+ return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
+ pipelineId: `${this.pipelineId}`,
+ }, false);
},
modalText() {
- return this.type === 'stop' ?
- sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
- pipelineId: `<strong>#${this.pipelineId}</strong>`,
- }, false) :
- sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), {
- pipelineId: `<strong>#${this.pipelineId}</strong>`,
- }, false);
- },
- primaryButtonLabel() {
- return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline');
+ return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
+ pipelineId: `<strong>#${this.pipelineId}</strong>`,
+ }, false);
},
},
created() {
@@ -73,10 +62,10 @@
setModalData(data) {
this.pipelineId = data.pipelineId;
this.endpoint = data.endpoint;
- this.type = data.type;
},
onSubmit() {
eventHub.$emit('postAction', this.endpoint);
+ this.cancelingPipeline = this.pipelineId;
},
},
};
@@ -119,21 +108,18 @@
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
+ :canceling-pipeline="cancelingPipeline"
/>
- <deprecated-modal
+
+ <modal
id="confirmation-modal"
- :title="modalTitle"
- :text="modalText"
- kind="danger"
- :primary-button-label="primaryButtonLabel"
+ :header-title-text="modalTitle"
+ footer-primary-button-variant="danger"
+ :footer-primary-button-text="s__('Pipeline|Stop pipeline')"
@submit="onSubmit"
>
- <template
- slot="body"
- slot-scope="props"
- >
- <p v-html="props.text"></p>
- </template>
- </deprecated-modal>
+ <span v-html="modalText"></span>
+ </modal>
+
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 4cbd67e0372..a3c17479e6f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -1,13 +1,15 @@
<script>
- /* eslint-disable no-param-reassign */
- import asyncButtonComponent from './async_button.vue';
- import pipelinesActionsComponent from './pipelines_actions.vue';
- import pipelinesArtifactsComponent from './pipelines_artifacts.vue';
- import ciBadge from '../../vue_shared/components/ci_badge_link.vue';
- import pipelineStage from './stage.vue';
- import pipelineUrl from './pipeline_url.vue';
- import pipelinesTimeago from './time_ago.vue';
- import commitComponent from '../../vue_shared/components/commit.vue';
+ import eventHub from '../event_hub';
+ import PipelinesActionsComponent from './pipelines_actions.vue';
+ import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
+ import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
+ import PipelineStage from './stage.vue';
+ import PipelineUrl from './pipeline_url.vue';
+ import PipelinesTimeago from './time_ago.vue';
+ import CommitComponent from '../../vue_shared/components/commit.vue';
+ import LoadingButton from '../../vue_shared/components/loading_button.vue';
+ import Icon from '../../vue_shared/components/icon.vue';
+ import { PIPELINES_TABLE } from '../constants';
/**
* Pipeline table row.
@@ -16,14 +18,15 @@
*/
export default {
components: {
- asyncButtonComponent,
- pipelinesActionsComponent,
- pipelinesArtifactsComponent,
- commitComponent,
- pipelineStage,
- pipelineUrl,
- ciBadge,
- pipelinesTimeago,
+ PipelinesActionsComponent,
+ PipelinesArtifactsComponent,
+ CommitComponent,
+ PipelineStage,
+ PipelineUrl,
+ CiBadge,
+ PipelinesTimeago,
+ LoadingButton,
+ Icon,
},
props: {
pipeline: {
@@ -43,6 +46,17 @@
type: String,
required: true,
},
+ cancelingPipeline: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ pipelinesTable: PIPELINES_TABLE,
+ data() {
+ return {
+ isRetrying: false,
+ };
},
computed: {
/**
@@ -119,8 +133,10 @@
if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') {
+ // eslint-disable-next-line no-param-reassign
accumulator.ref_url = this.pipeline.ref[prop];
} else {
+ // eslint-disable-next-line no-param-reassign
accumulator[prop] = this.pipeline.ref[prop];
}
return accumulator;
@@ -215,6 +231,23 @@
isChildView() {
return this.viewType === 'child';
},
+
+ isCancelling() {
+ return this.cancelingPipeline === this.pipeline.id;
+ },
+ },
+
+ methods: {
+ handleCancelClick() {
+ eventHub.$emit('openConfirmationModal', {
+ pipelineId: this.pipeline.id,
+ endpoint: this.pipeline.cancel_path,
+ });
+ },
+ handleRetryClick() {
+ this.isRetrying = true;
+ eventHub.$emit('retryPipeline', this.pipeline.retry_path);
+ },
},
};
</script>
@@ -272,6 +305,7 @@
v-for="(stage, index) in pipeline.details.stages"
:key="index">
<pipeline-stage
+ :type="$options.pipelinesTable"
:stage="stage"
:update-dropdown="updateGraphDropdown"
/>
@@ -287,7 +321,8 @@
<div
v-if="displayPipelineActions"
- class="table-section section-20 table-button-footer pipeline-actions">
+ class="table-section section-20 table-button-footer pipeline-actions"
+ >
<div class="btn-group table-action-buttons">
<pipelines-actions-component
v-if="pipeline.details.manual_actions.length"
@@ -296,33 +331,31 @@
<pipelines-artifacts-component
v-if="pipeline.details.artifacts.length"
- class="hidden-xs hidden-sm"
+ class="d-none d-sm-none d-md-block"
:artifacts="pipeline.details.artifacts"
/>
- <async-button-component
+ <loading-button
v-if="pipeline.flags.retryable"
- :endpoint="pipeline.retry_path"
- css-class="js-pipelines-retry-button btn-default btn-retry"
- title="Retry"
- icon="repeat"
- :pipeline-id="pipeline.id"
- data-toggle="modal"
- data-target="#confirmation-modal"
- type="retry"
- />
+ @click="handleRetryClick"
+ container-class="js-pipelines-retry-button btn btn-default btn-retry"
+ :loading="isRetrying"
+ :disabled="isRetrying"
+ >
+ <icon name="repeat" />
+ </loading-button>
- <async-button-component
+ <loading-button
v-if="pipeline.flags.cancelable"
- :endpoint="pipeline.cancel_path"
- css-class="js-pipelines-cancel-button btn-remove"
- title="Stop"
- icon="close"
- :pipeline-id="pipeline.id"
+ @click="handleCancelClick"
data-toggle="modal"
data-target="#confirmation-modal"
- type="stop"
- />
+ container-class="js-pipelines-cancel-button btn btn-remove"
+ :loading="isCancelling"
+ :disabled="isCancelling"
+ >
+ <icon name="close" />
+ </loading-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 32cf3dba3c3..f9769815796 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -1,135 +1,157 @@
<script>
-
- /**
- * Renders each stage of the pipeline mini graph.
- *
- * Given the provided endpoint will make a request to
- * fetch the dropdown data when the stage is clicked.
- *
- * Request is made inside this component to make it reusable between:
- * 1. Pipelines main table
- * 2. Pipelines table in commit and Merge request views
- * 3. Merge request widget
- * 4. Commit widget
- */
-
- import $ from 'jquery';
- import Flash from '../../flash';
- import axios from '../../lib/utils/axios_utils';
- import eventHub from '../event_hub';
- import Icon from '../../vue_shared/components/icon.vue';
- import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
-
- export default {
- components: {
- LoadingIcon,
- Icon,
+/**
+ * Renders each stage of the pipeline mini graph.
+ *
+ * Given the provided endpoint will make a request to
+ * fetch the dropdown data when the stage is clicked.
+ *
+ * Request is made inside this component to make it reusable between:
+ * 1. Pipelines main table
+ * 2. Pipelines table in commit and Merge request views
+ * 3. Merge request widget
+ * 4. Commit widget
+ */
+
+import $ from 'jquery';
+import { __ } from '../../locale';
+import Flash from '../../flash';
+import axios from '../../lib/utils/axios_utils';
+import eventHub from '../event_hub';
+import Icon from '../../vue_shared/components/icon.vue';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+import JobComponent from './graph/job_component.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import { PIPELINES_TABLE } from '../constants';
+
+export default {
+ components: {
+ LoadingIcon,
+ Icon,
+ JobComponent,
+ },
+
+ directives: {
+ tooltip,
+ },
+
+ props: {
+ stage: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ updateDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- props: {
- stage: {
- type: Object,
- required: true,
- },
-
- updateDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
+ type: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ dropdownContent: '',
+ };
+ },
+
+ computed: {
+ dropdownClass() {
+ return this.dropdownContent.length > 0
+ ? 'js-builds-dropdown-container'
+ : 'js-builds-dropdown-loading';
},
- data() {
- return {
- isLoading: false,
- dropdownContent: '',
- };
+ triggerButtonClass() {
+ return `ci-status-icon-${this.stage.status.group}`;
},
- computed: {
- dropdownClass() {
- return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
- },
+ borderlessIcon() {
+ return `${this.stage.status.icon}_borderless`;
+ },
+ },
- triggerButtonClass() {
- return `ci-status-icon-${this.stage.status.group}`;
- },
+ watch: {
+ updateDropdown() {
+ if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
+ this.fetchJobs();
+ }
+ },
+ },
+
+ updated() {
+ if (this.dropdownContent.length > 0) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ methods: {
+ onClickStage() {
+ if (!this.isDropdownOpen()) {
+ eventHub.$emit('clickedDropdown');
+ this.isLoading = true;
+ this.fetchJobs();
+ }
+ },
- borderlessIcon() {
- return `${this.stage.status.icon}_borderless`;
- },
+ fetchJobs() {
+ axios
+ .get(this.stage.dropdown_path)
+ .then(({ data }) => {
+ this.dropdownContent = data.latest_statuses;
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.closeDropdown();
+ this.isLoading = false;
+
+ Flash(__('Something went wrong on our end.'));
+ });
},
- watch: {
- updateDropdown() {
- if (this.updateDropdown &&
- this.isDropdownOpen() &&
- !this.isLoading) {
- this.fetchJobs();
- }
- },
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(
+ '.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
+ this.$el,
+ ).on('click', e => {
+ e.stopPropagation();
+ });
},
- updated() {
- if (this.dropdownContent.length > 0) {
- this.stopDropdownClickPropagation();
+ closeDropdown() {
+ if (this.isDropdownOpen()) {
+ $(this.$refs.dropdown).dropdown('toggle');
}
},
- methods: {
- onClickStage() {
- if (!this.isDropdownOpen()) {
- eventHub.$emit('clickedDropdown');
- this.isLoading = true;
- this.fetchJobs();
- }
- },
-
- fetchJobs() {
- axios.get(this.stage.dropdown_path)
- .then(({ data }) => {
- this.dropdownContent = data.html;
- this.isLoading = false;
- })
- .catch(() => {
- this.closeDropdown();
- this.isLoading = false;
-
- Flash('Something went wrong on our end.');
- });
- },
-
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
- *
- * Since this component is rendered multiple times per page we need to guarantee we only
- * target the click event of this component.
- */
- stopDropdownClickPropagation() {
- $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
- .on('click', (e) => {
- e.stopPropagation();
- });
- },
-
- closeDropdown() {
- if (this.isDropdownOpen()) {
- $(this.$refs.dropdown).dropdown('toggle');
- }
- },
-
- isDropdownOpen() {
- return this.$el.classList.contains('open');
- },
+ isDropdownOpen() {
+ return this.$el.classList.contains('open');
+ },
+
+ pipelineActionRequestComplete() {
+ if (this.type === PIPELINES_TABLE) {
+ // warn the table to update
+ eventHub.$emit('refreshPipelinesTable');
+ } else {
+ // close the dropdown in mr widget
+ $(this.$refs.dropdown).dropdown('toggle');
+ }
},
- };
+ },
+};
</script>
<template>
@@ -146,6 +168,7 @@
id="stageDropdown"
aria-haspopup="true"
aria-expanded="false"
+ ref="dropdown"
>
<span
@@ -168,7 +191,6 @@
>
<li
- :class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
>
@@ -176,8 +198,17 @@
<ul
v-else
- v-html="dropdownContent"
>
+ <li
+ v-for="job in dropdownContent"
+ :key="job.id"
+ >
+ <job-component
+ :job="job"
+ css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+ </li>
</ul>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue
index cd54d26c9d3..79dbdca4010 100644
--- a/app/assets/javascripts/pipelines/components/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/time_ago.vue
@@ -75,7 +75,7 @@
</p>
<p
- class="finished-at hidden-xs hidden-sm"
+ class="finished-at d-none d-sm-none d-md-block"
v-if="hasFinishedTime"
>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index b384c7500e7..eaa11a84cb9 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -1,2 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
+export const PIPELINES_TABLE = 'PIPELINES_TABLE';
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 6d87f75ae8e..30b1eee186d 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -53,11 +53,15 @@ export default {
});
eventHub.$on('postAction', this.postAction);
+ eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
+ eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
+ eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
+ eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
},
destroyed() {
this.poll.stop();
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 900eb7855f4..b49a16a87e6 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -25,28 +25,14 @@ export default () => {
data() {
return {
mediator,
- actionDisabled: null,
};
},
- created() {
- eventHub.$on('graphAction', this.postAction);
- },
- beforeDestroy() {
- eventHub.$off('graphAction', this.postAction);
- },
methods: {
- postAction(action) {
- this.actionDisabled = action;
-
- this.mediator.service.postAction(action)
- .then(() => {
- this.mediator.refreshPipeline();
- this.actionDisabled = null;
- })
- .catch(() => {
- this.actionDisabled = null;
- Flash(__('An error occurred while making the request.'));
- });
+ requestRefreshPipelineGraph() {
+ // When an action is clicked
+ // (wether in the dropdown or in the main nodes, we refresh the big graph)
+ this.mediator.refreshPipeline()
+ .catch(() => Flash(__('An error occurred while making the request.')));
},
},
render(createElement) {
@@ -54,7 +40,9 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
- actionDisabled: this.actionDisabled,
+ },
+ on: {
+ refreshPipelineGraph: this.requestRefreshPipelineGraph,
},
});
},
@@ -79,7 +67,8 @@ export default () => {
},
methods: {
postAction(action) {
- this.mediator.service.postAction(action.path)
+ this.mediator.service
+ .postAction(action.path)
.then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.')));
},
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index e5de3f69b01..a7a2a7235fd 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -86,7 +86,11 @@ Please update your Git repository remotes as soon as possible.`),
<div class="form-group">
<label :for="$options.inputId">{{ s__('Profiles|Path') }}</label>
<div class="input-group">
- <div class="input-group-addon">{{ rootUrl }}</div>
+ <div class="input-group-prepend">
+ <div class="input-group-text">
+ {{ rootUrl }}
+ </div>
+ </div>
<input
:id="$options.inputId"
class="form-control"
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index 6fedd94a6a9..f5cd1c3cc3e 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -4,6 +4,6 @@ export default () => {
$('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
if ($(this).hasClass('disabled')) return false;
- return $('.js-fork-content').toggle();
+ return $('.js-fork-content').toggleClass('hidden');
});
};
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index 72f03b02131..a5e68d1b0a0 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -39,7 +39,9 @@ export default class ProjectLabelSubscription {
const $button = $(button);
const originalTitle = $button.attr('data-original-title');
- if (originalTitle) ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction);
+ if (originalTitle) {
+ ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction);
+ }
return button;
});
@@ -51,6 +53,6 @@ export default class ProjectLabelSubscription {
const actionRegexp = new RegExp(newStatusVerb, 'i');
const newTitle = originalTitle.replace(actionRegexp, newAction);
- $button.tooltip('hide').attr('data-original-title', newTitle).tooltip('fixTitle');
+ $button.tooltip('hide').attr('data-original-title', newTitle).tooltip('_fixTitle');
}
}
diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js
index 7c95c71e239..a52ac768e57 100644
--- a/app/assets/javascripts/project_visibility.js
+++ b/app/assets/javascripts/project_visibility.js
@@ -7,7 +7,7 @@ function setVisibilityOptions(namespaceSelector) {
const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset;
- document.querySelectorAll('.visibility-level-setting .radio').forEach((option) => {
+ document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => {
const optionInput = option.querySelector('input[type=radio]');
const optionValue = optionInput ? optionInput.value : 0;
const optionTitle = option.querySelector('.option-title');
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 93603dfc14d..888b1d6ce33 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -66,8 +66,8 @@ const bindEvents = () => {
.on('click', (e) => { e.preventDefault(); })
.popover({
title: $pushNewProjectTipTrigger.data('title'),
- placement: 'auto bottom',
- html: 'true',
+ placement: 'bottom',
+ html: true,
content: $('.push-new-project-tip-template').html(),
})
.on('shown.bs.popover', () => {
diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue
index 34a60dd574b..0bbd8a41753 100644
--- a/app/assets/javascripts/projects_dropdown/components/app.vue
+++ b/app/assets/javascripts/projects_dropdown/components/app.vue
@@ -100,9 +100,10 @@ export default {
fetchSearchedProjects(searchQuery) {
this.searchQuery = searchQuery;
this.toggleLoader(true);
- this.service.getSearchedProjects(this.searchQuery)
+ this.service
+ .getSearchedProjects(this.searchQuery)
.then(res => res.json())
- .then((results) => {
+ .then(results => {
this.toggleSearchProjectsList(true);
this.store.setSearchedProjects(results);
})
diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue
index 0c46ed184be..7fcd62dcdb8 100644
--- a/app/assets/javascripts/projects_dropdown/components/search.vue
+++ b/app/assets/javascripts/projects_dropdown/components/search.vue
@@ -46,7 +46,7 @@
<template>
<div
- class="search-input-container hidden-xs"
+ class="search-input-container d-none d-sm-block"
>
<input
type="search"
diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js
index 7231f520933..ed1c3deead2 100644
--- a/app/assets/javascripts/projects_dropdown/service/projects_service.js
+++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js
@@ -50,7 +50,7 @@ export default class ProjectsService {
} else {
// Check if project is already present in frequents list
// When found, update metadata of it.
- storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => {
+ storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
if (projectItem.id === project.id) {
matchFound = true;
const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
@@ -104,13 +104,17 @@ export default class ProjectsService {
return [];
}
- if (bp.getBreakpointSize() === 'sm' ||
- bp.getBreakpointSize() === 'xs') {
+ if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
}
- const frequentProjects = storedFrequentProjects
- .filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY);
+ const frequentProjects = storedFrequentProjects.filter(
+ project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
+ );
+
+ if (!frequentProjects || frequentProjects.length === 0) {
+ return [];
+ }
// Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 0a60f4845b2..1a75fdd75db 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -31,7 +31,7 @@ export default class PrometheusMetrics {
/* eslint-disable class-methods-use-this */
handlePanelToggle(e) {
const $toggleBtn = $(e.currentTarget);
- const $currentPanelBody = $toggleBtn.closest('.panel').find('.panel-body');
+ const $currentPanelBody = $toggleBtn.closest('.card').find('.card-body');
$currentPanelBody.toggleClass('hidden');
if ($toggleBtn.hasClass('fa-caret-down')) {
$toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right');
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index a03180e80e6..2fc3778820b 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -28,11 +28,6 @@
isOpen: false,
};
},
- computed: {
- clipboardText() {
- return `docker pull ${this.repo.location}`;
- },
- },
methods: {
...mapActions([
'fetchRepos',
@@ -84,12 +79,12 @@
<clipboard-button
v-if="repo.location"
- :text="clipboardText"
+ :text="repo.location"
:title="repo.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
- <div class="controls hidden-xs pull-right">
+ <div class="controls d-none d-sm-block float-right">
<button
v-if="repo.canDelete"
type="button"
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index ee4eb3581f3..e4a4b3bb129 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -56,10 +56,6 @@
.catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
},
- clipboardText(text) {
- return `docker pull ${text}`;
- },
-
showError(message) {
Flash(errorMessages[message]);
},
@@ -89,7 +85,7 @@
<clipboard-button
v-if="item.location"
:title="item.location"
- :text="clipboardText(item.location)"
+ :text="item.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</td>
@@ -111,14 +107,20 @@
</td>
<td>
- {{ timeFormated(item.createdAt) }}
+ <span
+ v-tooltip
+ :title="tooltipTitle(item.createdAt)"
+ data-placement="bottom"
+ >
+ {{ timeFormated(item.createdAt) }}
+ </span>
</td>
<td class="content">
<button
v-if="item.canDelete"
type="button"
- class="js-delete-registry btn btn-danger hidden-xs pull-right"
+ class="js-delete-registry btn btn-danger d-none d-sm-block float-right"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
data-container="body"
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index 795b39bb3dc..593a43c7cc1 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js
index 588f479c492..f4923512578 100644
--- a/app/assets/javascripts/registry/stores/getters.js
+++ b/app/assets/javascripts/registry/stores/getters.js
@@ -1,2 +1,5 @@
export const isLoading = state => state.isLoading;
export const repos = state => state.repos;
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 2088a49590a..2afcf4626b8 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -5,6 +5,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this);
@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() {
};
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
- var $allGutterToggleIcons, $this, $thisIcon;
+ var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault();
$this = $(this);
- $thisIcon = $this.find('i');
+ isExpanded = $this.find('i').hasClass('fa-angle-double-right');
+ tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
$allGutterToggleIcons = $('.js-sidebar-toggle i');
- if ($thisIcon.hasClass('fa-angle-double-right')) {
+
+ if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
}
+
+ $this.attr('data-original-title', tooltipLabel);
+
if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
@@ -102,7 +108,7 @@ Sidebar.prototype.todoUpdateDone = function(data) {
.attr('title', $el.data(`${attrPrefix}Text`));
if ($el.hasClass('has-tooltip')) {
- $el.tooltip('fixTitle');
+ $el.tooltip('_fixTitle');
}
if ($el.data(`${attrPrefix}Icon`)) {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index e31e067033f..99c71d6524a 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -85,6 +85,7 @@ export default class Shortcuts {
if ($modal.length) {
$modal.modal('toggle');
+ return null;
}
return axios.get(gon.shortcuts_path, {
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index a4d10850471..78f7353eb0d 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts {
super();
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
- Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
+ Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
@@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
- Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
+ Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
+ Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project');
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index 5eeb2a41bae..284a258d3c9 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -41,7 +41,7 @@ export default {
</i>
<a
v-if="editable"
- class="js-sidebar-dropdown-toggle edit-link pull-right"
+ class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
>
{{ __('Edit') }}
@@ -49,7 +49,7 @@ export default {
<a
v-if="showToggle"
aria-label="Toggle sidebar"
- class="gutter-toggle pull-right js-sidebar-toggle"
+ class="gutter-toggle float-right js-sidebar-toggle"
href="#"
role="button"
>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 1e7f46454bf..5a374d84796 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,6 +1,12 @@
<script>
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
export default {
name: 'Assignees',
+ directives: {
+ tooltip,
+ },
props: {
rootPath: {
type: String,
@@ -14,6 +20,11 @@ export default {
type: Boolean,
required: true,
},
+ issuableType: {
+ type: String,
+ require: true,
+ default: 'issue',
+ },
},
data() {
return {
@@ -62,6 +73,12 @@ export default {
names.push(`+ ${this.users.length - maxRender} more`);
}
+ if (!this.users.length) {
+ const emptyTooltipLabel = this.issuableType === 'issue' ?
+ __('Assignee(s)') : __('Assignee');
+ names.push(emptyTooltipLabel);
+ }
+
return names.join(', ');
},
sidebarAvatarCounter() {
@@ -109,9 +126,11 @@ export default {
<div>
<div
class="sidebar-collapsed-icon sidebar-collapsed-user"
- :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
+ :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ v-tooltip
data-container="body"
data-placement="left"
+ data-boundary="viewport"
:title="collapsedTooltipTitle"
>
<i
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 3c6b9c27814..b04a2eff798 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,9 +1,9 @@
<script>
-import Flash from '../../../flash';
+import Flash from '~/flash';
+import eventHub from '~/sidebar/event_hub';
+import Store from '~/sidebar/stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
-import Store from '../../stores/sidebar_store';
-import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
@@ -25,6 +25,11 @@ export default {
required: false,
default: false,
},
+ issuableType: {
+ type: String,
+ require: true,
+ default: 'issue',
+ },
},
data() {
return {
@@ -90,6 +95,7 @@ export default {
:users="store.assignees"
:editable="store.editable"
@assign-self="assignSelf"
+ :issuable-type="issuableType"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index ceb02309959..3f6e2f05396 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,15 +1,19 @@
<script>
-import Flash from '../../../flash';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
-import Icon from '../../../vue_shared/components/icon.vue';
-import { __ } from '../../../locale';
-import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
+ directives: {
+ tooltip,
+ },
props: {
isConfidential: {
required: true,
@@ -33,6 +37,9 @@ export default {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
},
+ tooltipLabel() {
+ return this.isConfidential ? __('Confidential') : __('Not confidential');
+ },
},
created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm);
@@ -65,6 +72,11 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ :title="tooltipLabel"
>
<icon
:name="confidentialityIcon"
@@ -75,7 +87,7 @@ export default {
{{ __('Confidentiality') }}
<a
v-if="isEditable"
- class="pull-right confidential-edit"
+ class="float-right confidential-edit"
href="#"
@click.prevent="toggleForm"
>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index 3783f71a848..4165aa19acf 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -32,7 +32,7 @@ export default {
</script>
<template>
- <div class="dropdown open">
+ <div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
<p
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index e1e4715826a..d392977e5e2 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -41,7 +41,7 @@ export default {
</script>
<template>
- <div class="dropdown open">
+ <div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<p
class="text"
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index e4893451af3..fb69c741dcd 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,15 +1,22 @@
<script>
+import { __ } from '~/locale';
import Flash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import issuableMixin from '~/vue_shared/mixins/issuable';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
-import issuableMixin from '../../../vue_shared/mixins/issuable';
-import Icon from '../../../vue_shared/components/icon.vue';
-import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
+
+ directives: {
+ tooltip,
+ },
+
mixins: [issuableMixin],
props: {
@@ -44,6 +51,10 @@ export default {
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
+
+ tooltipLabel() {
+ return this.isLocked ? __('Locked') : __('Unlocked');
+ },
},
created() {
@@ -85,6 +96,11 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ :title="tooltipLabel"
>
<icon
:name="lockIcon"
@@ -97,7 +113,7 @@ export default {
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<button
v-if="isEditable"
- class="pull-right lock-edit"
+ class="float-right lock-edit"
type="button"
@click.prevent="toggleForm"
>
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 006a6d2905d..0a945fc7fd5 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,9 +1,13 @@
<script>
- import { __, n__, sprintf } from '../../../locale';
- import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
- import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
+ import { __, n__, sprintf } from '~/locale';
+ import tooltip from '~/vue_shared/directives/tooltip';
+ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
+ directives: {
+ tooltip,
+ },
components: {
loadingIcon,
userAvatarImage,
@@ -66,13 +70,24 @@
toggleMoreParticipants() {
this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
},
+ onClickCollapsedIcon() {
+ this.$emit('toggleSidebar');
+ },
},
};
</script>
<template>
<div>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ :title="participantLabel"
+ @click="onClickCollapsedIcon"
+ >
<i
class="fa fa-users"
aria-hidden="true"
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 3e8cc7a6630..385717e7c1e 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -1,6 +1,5 @@
<script>
import Store from '../../stores/sidebar_store';
-import eventHub from '../../event_hub';
import Flash from '../../../flash';
import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
@@ -20,12 +19,6 @@ export default {
store: new Store(),
};
},
- created() {
- eventHub.$on('toggleSubscription', this.onToggleSubscription);
- },
- beforeDestroy() {
- eventHub.$off('toggleSubscription', this.onToggleSubscription);
- },
methods: {
onToggleSubscription() {
this.mediator.toggleSubscription()
@@ -42,6 +35,7 @@ export default {
<subscriptions
:loading="store.isFetching.subscriptions"
:subscribed="store.subscribed"
+ @toggleSubscription="onToggleSubscription"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index d69d100a26c..6745c1aafff 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -47,8 +47,25 @@
},
},
methods: {
+ /**
+ * We need to emit this event on both component & eventHub
+ * for 2 dependencies;
+ *
+ * 1. eventHub: This component is used in Issue Boards sidebar
+ * where component template is part of HAML
+ * and event listeners are tied to app's eventHub.
+ * 2. Component: This compone is also used in Epics in EE
+ * where listeners are tied to component event.
+ */
toggleSubscription() {
+ // App's eventHub event emission.
eventHub.$emit('toggleSubscription', this.id);
+
+ // Component event emission.
+ this.$emit('toggleSubscription', this.id);
+ },
+ onClickCollapsedIcon() {
+ this.$emit('toggleSidebar');
},
},
};
@@ -56,12 +73,16 @@
<template>
<div>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="onClickCollapsedIcon"
+ >
<span
v-tooltip
:title="notificationTooltip"
data-container="body"
data-placement="left"
+ data-boundary="viewport"
>
<icon
:name="notificationIcon"
@@ -71,12 +92,12 @@
/>
</span>
</div>
- <span class="issuable-header-text hide-collapsed pull-left">
+ <span class="issuable-header-text hide-collapsed float-left">
{{ __('Notifications') }}
</span>
<toggle-button
ref="toggleButton"
- class="pull-right hide-collapsed js-issuable-subscribe-button"
+ class="float-right hide-collapsed js-issuable-subscribe-button"
:is-loading="showLoadingState"
:value="subscribed"
@change="toggleSubscription"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 3b86f1145d1..209af1ce152 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,12 +1,17 @@
<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import { abbreviateTime } from '../../../lib/utils/pretty_time';
+ import { __, sprintf } from '~/locale';
+ import { abbreviateTime } from '~/lib/utils/pretty_time';
+ import icon from '~/vue_shared/components/icon.vue';
+ import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
components: {
icon,
},
+ directives: {
+ tooltip,
+ },
props: {
showComparisonState: {
type: Boolean,
@@ -79,6 +84,21 @@
return '';
},
+ timeTrackedTooltipText() {
+ let title;
+ if (this.showComparisonState) {
+ title = __('Time remaining');
+ } else if (this.showEstimateOnlyState) {
+ title = __('Estimated');
+ } else if (this.showSpentOnlyState) {
+ title = __('Time spent');
+ }
+
+ return sprintf('%{title}: %{text}', ({ title, text: this.text }));
+ },
+ tooltipText() {
+ return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
+ },
},
methods: {
abbreviateTime(timeStr) {
@@ -89,7 +109,14 @@
</script>
<template>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ :title="tooltipText"
+ >
<icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index 82c4562f9a9..6f79310b1cc 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -71,7 +71,7 @@ export default {
</div>
</div>
<div class="compare-display-container">
- <div class="compare-display pull-left">
+ <div class="compare-display float-left">
<span class="compare-label">
{{ s__('TimeTracking|Spent') }}
</span>
@@ -79,7 +79,7 @@ export default {
{{ timeSpentHumanReadable }}
</span>
</div>
- <div class="compare-display estimated pull-right">
+ <div class="compare-display estimated float-right">
<span class="compare-label">
{{ s__('TimeTrackingEstimated|Est') }}
</span>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
deleted file mode 100644
index 38da76c6771..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export default {
- 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/sidebar/components/time_tracking/no_tracking_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue
new file mode 100644
index 00000000000..9228184df5b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue
@@ -0,0 +1,13 @@
+<script>
+export default {
+ name: 'TimeTrackingNoTrackingPane',
+};
+</script>
+
+<template>
+ <div class="time-tracking-no-tracking-pane">
+ <span class="no-value">
+ {{ __('No estimate or time spent') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 5626cccc022..2e1d6e9643a 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -1,3 +1,4 @@
+<script>
import $ from 'jquery';
import _ from 'underscore';
@@ -10,14 +11,17 @@ import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
export default {
+ components: {
+ IssuableTimeTracker,
+ },
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
- components: {
- IssuableTimeTracker,
+ mounted() {
+ this.listenForQuickActions();
},
methods: {
listenForQuickActions() {
@@ -41,18 +45,17 @@ export default {
}
},
},
- mounted() {
- this.listenForQuickActions();
- },
- template: `
- <div class="block">
- <issuable-time-tracker
- :time_estimate="store.timeEstimate"
- :time_spent="store.totalTimeSpent"
- :human_time_estimate="store.humanTimeEstimate"
- :human_time_spent="store.humanTotalTimeSpent"
- :rootPath="store.rootPath"
- />
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="block">
+ <issuable-time-tracker
+ :time_estimate="store.timeEstimate"
+ :time_spent="store.totalTimeSpent"
+ :human_time_estimate="store.humanTimeEstimate"
+ :human_time_spent="store.humanTotalTimeSpent"
+ :root-path="store.rootPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js
deleted file mode 100644
index bf987562647..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export default {
- name: 'time-tracking-spent-only-pane',
- props: {
- timeSpentHumanReadable: {
- type: String,
- required: true,
- },
- },
- template: `
- <div class="time-tracking-spend-only-pane">
- <span class="bold">Spent:</span>
- {{ timeSpentHumanReadable }}
- </div>
- `,
-};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
new file mode 100644
index 00000000000..59cd99f8f14
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
@@ -0,0 +1,18 @@
+<script>
+export default {
+ name: 'TimeTrackingSpentOnlyPane',
+ props: {
+ timeSpentHumanReadable: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="time-tracking-spend-only-pane">
+ <span class="bold">Spent:</span>
+ {{ timeSpentHumanReadable }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 71dca498b3d..7d56d2fa5ee 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,8 +1,8 @@
<script>
import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue';
-import timeTrackingSpentOnlyPane from './spent_only_pane';
-import timeTrackingNoTrackingPane from './no_tracking_pane';
+import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
+import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
@@ -13,8 +13,8 @@ export default {
components: {
TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane,
- 'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
- 'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
+ TimeTrackingSpentOnlyPane,
+ TimeTrackingNoTrackingPane,
TimeTrackingComparisonPane,
TimeTrackingHelpState,
},
@@ -116,7 +116,7 @@ export default {
<div class="title hide-collapsed">
{{ __('Time tracking') }}
<div
- class="help-button pull-right"
+ class="help-button float-right"
v-if="!showHelpState"
@click="toggleHelpState(true)"
>
@@ -127,7 +127,7 @@ export default {
</i>
</div>
<div
- class="close-help-button pull-right"
+ class="close-help-button float-right"
v-if="showHelpState"
@click="toggleHelpState(false)"
>
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 1eadebc7004..b267422cd97 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import _ from 'underscore';
function isValidProjectId(id) {
return id > 0;
@@ -43,7 +44,7 @@ class SidebarMoveIssue {
renderRow: project => `
<li>
<a href="#" class="js-move-issue-dropdown-item">
- ${project.name_with_namespace}
+ ${_.escape(project.name_with_namespace)}
</a>
</li>
`,
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 9f5d852260e..3086e7d0fc9 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
+import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) {
mediator,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
+ issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
},
}),
});
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
index 97d5cf96bcb..96dfff77859 100644
--- a/app/assets/javascripts/user_callout.js
+++ b/app/assets/javascripts/user_callout.js
@@ -15,7 +15,7 @@ export default class UserCallout {
init() {
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
- $('.js-close-callout').on('click', e => this.dismissCallout(e));
+ this.userCalloutBody.find('.js-close-callout').on('click', e => this.dismissCallout(e));
}
}
@@ -23,12 +23,15 @@ export default class UserCallout {
const $currentTarget = $(e.currentTarget);
if (this.options.setCalloutPerProject) {
- Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('projectPath') });
+ Cookies.set(this.cookieName, 'true', {
+ expires: 365,
+ path: this.userCalloutBody.data('projectPath'),
+ });
} else {
Cookies.set(this.cookieName, 'true', { expires: 365 });
}
- if ($currentTarget.hasClass('close')) {
+ if ($currentTarget.hasClass('close') || $currentTarget.hasClass('js-close')) {
this.userCalloutBody.remove();
}
}
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 520a0b3f424..cd954f75613 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -5,6 +5,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor
@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) {
return axios.put(issueURL, data)
.then(({ data }) => {
- var user;
+ var user, tooltipTitle;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) {
username: data.assignee.username,
avatar: data.assignee.avatar_url
};
+ tooltipTitle = _.escape(user.name);
} else {
user = {
name: 'Unassigned',
username: '',
avatar: ''
};
+ tooltipTitle = __('Assignee');
}
$value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
+ $collapsedSidebar.attr('title', tooltipTitle).tooltip('_fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 7bef2e97349..1608858da22 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -109,12 +109,12 @@ export default {
rel="noopener noreferrer nofollow"
class="deploy-link js-deploy-url"
>
+ {{ deployment.external_url_formatted }}
<i
class="fa fa-external-link"
aria-hidden="true"
>
</i>
- {{ deployment.external_url_formatted }}
</a>
</template>
<span
@@ -127,7 +127,7 @@ export default {
</span>
<loading-button
v-if="deployment.stop_url"
- container-class="btn btn-default btn-xs prepend-left-default"
+ container-class="btn btn-default btn-sm prepend-left-default"
label="Stop environment"
:loading="isStopping"
@click="stopEnvironment"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
index cb6e9858736..8338fde61c7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -2,7 +2,7 @@
import tooltip from '../../vue_shared/directives/tooltip';
export default {
- name: 'MRWidgetAuthor',
+ name: 'MrWidgetAuthor',
directives: {
tooltip,
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
index 8f1fd809a81..644e4b7d81a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
@@ -1,10 +1,10 @@
<script>
- import mrWidgetAuthor from './mr_widget_author.vue';
+ import MrWidgetAuthor from './mr_widget_author.vue';
export default {
name: 'MRWidgetAuthorTime',
components: {
- mrWidgetAuthor,
+ MrWidgetAuthor,
},
props: {
actionText: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 18ee4c62bf1..51a0fda6555 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -131,7 +131,7 @@ export default {
aria-hidden="true">
</i>
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li>
<a
class="js-download-email-patches"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue
deleted file mode 100644
index f0298f732ea..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<script>
- export default {
- name: 'MRWidgetMaintainerEdit',
- props: {
- maintainerEditAllowed: {
- type: Boolean,
- default: false,
- required: false,
- },
- },
- };
-</script>
-
-<template>
- <section class="mr-info-list mr-links">
- <p v-if="maintainerEditAllowed">
- {{ s__("mrWidget|Allows edits from maintainers") }}
- </p>
- </section>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index ebaf2b972eb..b404a592234 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -42,7 +42,7 @@
@click="refreshWidget"
:disabled="isRefreshing"
type="button"
- class="btn btn-xs btn-default"
+ class="btn btn-sm btn-default"
>
<loading-icon
v-if="isRefreshing"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index dad4b0fe49d..5c1500ab801 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -43,13 +43,13 @@ To merge this request, first rebase locally.`) }}
<a
v-if="mr.canMerge && mr.conflictResolutionPath"
:href="mr.conflictResolutionPath"
- class="js-resolve-conflicts-button btn btn-default btn-xs"
+ class="js-resolve-conflicts-button btn btn-default btn-sm"
>
{{ s__("mrWidget|Resolve conflicts") }}
</a>
<button
v-if="mr.canMerge"
- class="js-merge-locally-button btn btn-default btn-xs"
+ class="js-merge-locally-button btn btn-default btn-sm"
data-toggle="modal"
data-target="#modal_merge_info"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index 602b68ea572..05fecd4de35 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -1,66 +1,70 @@
<script>
- import { n__ } from '~/locale';
- import statusIcon from '../mr_widget_status_icon.vue';
- import eventHub from '../../event_hub';
+import { n__ } from '~/locale';
+import statusIcon from '../mr_widget_status_icon.vue';
+import eventHub from '../../event_hub';
- export default {
- name: 'MRWidgetFailedToMerge',
+export default {
+ name: 'MRWidgetFailedToMerge',
- components: {
- statusIcon,
- },
+ components: {
+ statusIcon,
+ },
- props: {
- mr: {
- type: Object,
- required: true,
- default: () => ({}),
- },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ default: () => ({}),
},
+ },
- data() {
- return {
- timer: 10,
- isRefreshing: false,
- };
- },
+ data() {
+ return {
+ timer: 10,
+ isRefreshing: false,
+ intervalId: null,
+ };
+ },
- computed: {
- timerText() {
- return n__(
- 'Refreshing in a second to show the updated status...',
- 'Refreshing in %d seconds to show the updated status...',
- this.timer,
- );
- },
+ computed: {
+ timerText() {
+ return n__(
+ 'Refreshing in a second to show the updated status...',
+ 'Refreshing in %d seconds to show the updated status...',
+ this.timer,
+ );
},
+ },
- mounted() {
- setInterval(() => {
- this.updateTimer();
- }, 1000);
- },
+ mounted() {
+ this.intervalId = setInterval(this.updateTimer, 1000);
+ },
- created() {
- eventHub.$emit('DisablePolling');
- },
+ created() {
+ eventHub.$emit('DisablePolling');
+ },
- methods: {
- refresh() {
- this.isRefreshing = true;
- eventHub.$emit('MRWidgetUpdateRequested');
- eventHub.$emit('EnablePolling');
- },
- updateTimer() {
- this.timer = this.timer - 1;
+ beforeDestroy() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ }
+ },
- if (this.timer === 0) {
- this.refresh();
- }
- },
+ methods: {
+ refresh() {
+ this.isRefreshing = true;
+ eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('EnablePolling');
},
+ updateTimer() {
+ this.timer = this.timer - 1;
- };
+ if (this.timer === 0) {
+ this.refresh();
+ }
+ },
+ },
+};
</script>
<template>
<div class="mr-widget-body media">
@@ -94,7 +98,7 @@
</span>
<button
@click="refresh"
- class="btn btn-default btn-xs js-refresh-button"
+ class="btn btn-default btn-sm js-refresh-button"
type="button"
>
{{ s__("mrWidget|Refresh now") }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
index 7ff7fc7988a..e8352c362d6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
@@ -1,13 +1,13 @@
<script>
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
- import mrWidgetAuthor from '../../components/mr_widget_author.vue';
+ import MrWidgetAuthor from '../../components/mr_widget_author.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMergeWhenPipelineSucceeds',
components: {
- mrWidgetAuthor,
+ MrWidgetAuthor,
statusIcon,
},
props: {
@@ -94,7 +94,7 @@
:disabled="isCancellingAutoMerge"
role="button"
href="#"
- class="btn btn-xs btn-default js-cancel-auto-merge">
+ class="btn btn-sm btn-default js-cancel-auto-merge">
<i
v-if="isCancellingAutoMerge"
class="fa fa-spinner fa-spin"
@@ -129,7 +129,7 @@
:disabled="isRemovingSourceBranch"
@click.prevent="removeSourceBranch"
role="button"
- class="btn btn-xs btn-default js-remove-source-branch"
+ class="btn btn-sm btn-default js-remove-source-branch"
href="#"
>
<i
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index c1618bc6ea0..8c7e0664559 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -3,6 +3,7 @@
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
+ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
@@ -16,6 +17,7 @@
mrWidgetAuthorTime,
loadingIcon,
statusIcon,
+ ClipboardButton,
},
props: {
mr: {
@@ -116,7 +118,7 @@
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip
- class="btn btn-close btn-xs"
+ class="btn btn-close btn-sm"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
@@ -127,7 +129,7 @@
<a
v-else-if="mr.revertInForkPath"
v-tooltip
- class="btn btn-close btn-xs"
+ class="btn btn-close btn-sm"
data-method="post"
:href="mr.revertInForkPath"
:title="revertTitle"
@@ -137,7 +139,7 @@
<a
v-if="mr.canCherryPickInCurrentMR"
v-tooltip
- class="btn btn-default btn-xs"
+ class="btn btn-default btn-sm"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
@@ -148,7 +150,7 @@
<a
v-else-if="mr.cherryPickInForkPath"
v-tooltip
- class="btn btn-default btn-xs"
+ class="btn btn-default btn-sm"
data-method="post"
:href="mr.cherryPickInForkPath"
:title="cherryPickTitle"
@@ -162,6 +164,18 @@
<span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</span>
+ with
+ <a
+ :href="mr.mergeCommitPath"
+ class="commit-sha js-mr-merged-commit-sha"
+ >
+ {{ mr.shortMergeCommitSha }}
+ </a>
+ <clipboard-button
+ :title="__('Copy commit SHA to clipboard')"
+ :text="mr.shortMergeCommitSha"
+ css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
+ />
</p>
<p v-if="mr.sourceBranchRemoved">
{{ s__("mrWidget|The source branch has been removed") }}
@@ -175,7 +189,7 @@
@click="removeSourceBranch"
:disabled="isMakingRequest"
type="button"
- class="btn btn-xs btn-default js-remove-branch-button"
+ class="btn btn-sm btn-default js-remove-branch-button"
>
{{ s__("mrWidget|Remove Source Branch") }}
</button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
index bf8628d18a6..926a3172412 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
@@ -10,6 +10,6 @@ In EE, the configuration extends this object to add a functioning squash-before-
button.
*/
-export default {
- template: '',
-};
+<script>
+ export default {};
+</script>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 3d9161f6926..086dbabe77e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -18,10 +18,10 @@ export default {
<template>
<div class="mr-widget-body mr-widget-empty-state">
<div class="row">
- <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center">
+ <div class="artwork col-md-5 order-md-last col-12 text-center">
<span v-html="emptyStateSVG"></span>
</div>
- <div class="text col-sm-7 col-sm-pull-5 col-xs-12">
+ <div class="text col-md-7 order-md-first col-12">
<span>
Merge requests are a place to propose changes you have made to a project
and discuss those changes with others.
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 0264625a526..1d1c8ebc179 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -312,7 +312,7 @@ export default {
v-else
@click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
- class="js-modify-commit-message-button btn btn-default btn-xs"
+ class="js-modify-commit-message-button btn btn-default btn-sm"
type="button">
Modify commit message
</button>
@@ -329,7 +329,7 @@ export default {
class="prepend-top-default commit-message-editor">
<div class="form-group clearfix">
<label
- class="control-label"
+ class="col-form-label"
for="commit-message">
Commit message
</label>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index a1f7e696795..8ea3f22ecc2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -28,7 +28,7 @@ export default {
<a
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
- class="btn btn-default btn-xs js-create-issue"
+ class="btn btn-default btn-sm js-create-issue"
>
{{ s__("mrWidget|Create an issue to resolve them later") }}
</a>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 44e1a616a19..fe2608e8212 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -1,25 +1,26 @@
+<script>
import $ from 'jquery';
import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
export default {
- name: 'MRWidgetWIP',
- props: {
- mr: { type: Object, required: true },
- service: { type: Object, required: true },
+ name: 'WorkInProgress',
+ components: {
+ statusIcon,
},
directives: {
tooltip,
},
+ props: {
+ mr: { type: Object, required: true },
+ service: { type: Object, required: true },
+ },
data() {
return {
isMakingRequest: false,
};
},
- components: {
- statusIcon,
- },
methods: {
removeWIP() {
this.isMakingRequest = true;
@@ -36,32 +37,40 @@ export default {
});
},
},
- template: `
- <div class="mr-widget-body media">
- <status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" />
- <div class="media-body space-children">
- <span class="bold">
- This is a Work in Progress
- <i
- v-tooltip
- class="fa fa-question-circle"
- title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
- aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
- </i>
- </span>
- <button
- v-if="mr.removeWIPPath"
- @click="removeWIP"
- :disabled="isMakingRequest"
- type="button"
- class="btn btn-default btn-xs js-remove-wip">
- <i
- v-if="isMakingRequest"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- Resolve WIP status
- </button>
- </div>
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon
+ status="warning"
+ :show-disabled-button="Boolean(mr.removeWIPPath)"
+ />
+ <div class="media-body space-children">
+ <span class="bold">
+ This is a Work in Progress
+ <i
+ v-tooltip
+ class="fa fa-question-circle"
+ title="When this merge request is ready,
+ remove the WIP: prefix from the title to allow it to be merged"
+ aria-label="When this merge request is ready,
+ remove the WIP: prefix from the title to allow it to be merged">
+ </i>
+ </span>
+ <button
+ v-if="mr.removeWIPPath"
+ @click="removeWIP"
+ :disabled="isMakingRequest"
+ type="button"
+ class="btn btn-default btn-xs js-remove-wip">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true">
+ </i>
+ Resolve WIP status
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 3b5c973e4a0..15097fa2a3f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -15,13 +15,12 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as Deployment } from './components/deployment.vue';
-export { default as WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.vue';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
export { default as MergedState } from './components/states/mr_widget_merged.vue';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
export { default as ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue';
-export { default as WipState } from './components/states/mr_widget_wip';
+export { default as WorkInProgressState } from './components/states/work_in_progress.vue';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
@@ -41,8 +40,8 @@ export { default as MRWidgetService } from './services/mr_widget_service';
export { default as eventHub } from './event_hub';
export { default as getStateKey } from './stores/get_state_key';
export { default as stateMaps } from './stores/state_maps';
-export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge';
+export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge.vue';
export { default as notify } from '../lib/utils/notify';
export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue';
-export { default as mrWidgetOptions } from './mr_widget_options';
+export { default as mrWidgetOptions } from './mr_widget_options.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 0be5d9e5a55..f69fe03fcb3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,18 +1,19 @@
+<script>
+
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
-import Flash from '../flash';
+import createFlash from '../flash';
import {
WidgetHeader,
WidgetMergeHelp,
WidgetPipeline,
Deployment,
- WidgetMaintainerEdit,
WidgetRelatedLinks,
MergedState,
ClosedState,
MergingState,
RebaseState,
- WipState,
+ WorkInProgressState,
ArchivedState,
ConflictsState,
NothingToMergeState,
@@ -40,10 +41,39 @@ import { setFavicon } from '../lib/utils/common_utils';
export default {
el: '#js-vue-mr-widget',
name: 'MRWidget',
+ components: {
+ 'mr-widget-header': WidgetHeader,
+ 'mr-widget-merge-help': WidgetMergeHelp,
+ 'mr-widget-pipeline': WidgetPipeline,
+ Deployment,
+ 'mr-widget-related-links': WidgetRelatedLinks,
+ 'mr-widget-merged': MergedState,
+ 'mr-widget-closed': ClosedState,
+ 'mr-widget-merging': MergingState,
+ 'mr-widget-failed-to-merge': FailedToMerge,
+ 'mr-widget-wip': WorkInProgressState,
+ 'mr-widget-archived': ArchivedState,
+ 'mr-widget-conflicts': ConflictsState,
+ 'mr-widget-nothing-to-merge': NothingToMergeState,
+ 'mr-widget-not-allowed': NotAllowedState,
+ 'mr-widget-missing-branch': MissingBranchState,
+ 'mr-widget-ready-to-merge': ReadyToMergeState,
+ 'sha-mismatch': ShaMismatchState,
+ 'mr-widget-squash-before-merge': SquashBeforeMerge,
+ 'mr-widget-checking': CheckingState,
+ 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
+ 'mr-widget-pipeline-blocked': PipelineBlockedState,
+ 'mr-widget-pipeline-failed': PipelineFailedState,
+ 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
+ 'mr-widget-auto-merge-failed': AutoMergeFailed,
+ 'mr-widget-rebase': RebaseState,
+ SourceBranchRemovalStatus,
+ },
props: {
mrData: {
type: Object,
required: false,
+ default: null,
},
},
data() {
@@ -72,6 +102,13 @@ export default {
(!this.mr.isNothingToMergeState && !this.mr.isMergedState);
},
},
+ created() {
+ this.initPolling();
+ this.bindEventHubListeners();
+ },
+ mounted() {
+ this.handleMounted();
+ },
methods: {
createService(store) {
const endpoints = {
@@ -99,7 +136,7 @@ export default {
cb.call(null, data);
}
})
- .catch(() => new Flash('Something went wrong. Please try again.'));
+ .catch(() => createFlash('Something went wrong. Please try again.'));
},
initPolling() {
this.pollingInterval = new SmartInterval({
@@ -134,7 +171,7 @@ export default {
}
})
.catch(() => {
- new Flash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line
+ createFlash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line
});
},
fetchActionsContent() {
@@ -147,7 +184,7 @@ export default {
Project.initRefSwitcher();
}
})
- .catch(() => new Flash('Something went wrong. Please try again.'));
+ .catch(() => createFlash('Something went wrong. Please try again.'));
},
handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return;
@@ -202,76 +239,53 @@ export default {
this.initDeploymentsPolling();
},
},
- created() {
- this.initPolling();
- this.bindEventHubListeners();
- },
- mounted() {
- this.handleMounted();
- },
- components: {
- 'mr-widget-header': WidgetHeader,
- 'mr-widget-merge-help': WidgetMergeHelp,
- 'mr-widget-pipeline': WidgetPipeline,
- Deployment,
- 'mr-widget-maintainer-edit': WidgetMaintainerEdit,
- 'mr-widget-related-links': WidgetRelatedLinks,
- 'mr-widget-merged': MergedState,
- 'mr-widget-closed': ClosedState,
- 'mr-widget-merging': MergingState,
- 'mr-widget-failed-to-merge': FailedToMerge,
- 'mr-widget-wip': WipState,
- 'mr-widget-archived': ArchivedState,
- 'mr-widget-conflicts': ConflictsState,
- 'mr-widget-nothing-to-merge': NothingToMergeState,
- 'mr-widget-not-allowed': NotAllowedState,
- 'mr-widget-missing-branch': MissingBranchState,
- 'mr-widget-ready-to-merge': ReadyToMergeState,
- 'mr-widget-sha-mismatch': ShaMismatchState,
- 'mr-widget-squash-before-merge': SquashBeforeMerge,
- 'mr-widget-checking': CheckingState,
- 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
- 'mr-widget-pipeline-blocked': PipelineBlockedState,
- 'mr-widget-pipeline-failed': PipelineFailedState,
- 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
- 'mr-widget-auto-merge-failed': AutoMergeFailed,
- 'mr-widget-rebase': RebaseState,
- SourceBranchRemovalStatus,
- },
- template: `
- <div class="mr-state-widget prepend-top-default">
- <mr-widget-header :mr="mr" />
- <mr-widget-pipeline
- v-if="shouldRenderPipelines"
- :pipeline="mr.pipeline"
- :ci-status="mr.ciStatus"
- :has-ci="mr.hasCI"
- />
- <deployment
- v-for="deployment in mr.deployments"
- :key="deployment.id"
- :deployment="deployment"
+};
+</script>
+<template>
+ <div class="mr-state-widget prepend-top-default">
+ <mr-widget-header
+ :mr="mr"
+ />
+ <mr-widget-pipeline
+ v-if="shouldRenderPipelines"
+ :pipeline="mr.pipeline"
+ :ci-status="mr.ciStatus"
+ :has-ci="mr.hasCI"
+ />
+ <deployment
+ v-for="deployment in mr.deployments"
+ :key="deployment.id"
+ :deployment="deployment"
+ />
+ <div class="mr-widget-section">
+ <component
+ :is="componentName"
+ :mr="mr"
+ :service="service"
+ />
+
+ <section
+ v-if="mr.maintainerEditAllowed"
+ class="mr-info-list mr-links"
+ >
+ {{ s__("mrWidget|Allows edits from maintainers") }}
+ </section>
+
+ <mr-widget-related-links
+ v-if="shouldRenderRelatedLinks"
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ />
+
+ <source-branch-removal-status
+ v-if="shouldRenderSourceBranchRemovalStatus"
/>
- <div class="mr-widget-section">
- <component
- :is="componentName"
- :mr="mr"
- :service="service" />
- <mr-widget-maintainer-edit
- :maintainerEditAllowed="mr.maintainerEditAllowed" />
- <mr-widget-related-links
- v-if="shouldRenderRelatedLinks"
- :state="mr.state"
- :related-links="mr.relatedLinks" />
- <source-branch-removal-status
- v-if="shouldRenderSourceBranchRemovalStatus"
- />
- </div>
- <div
- class="mr-widget-footer"
- v-if="shouldRenderMergeHelp">
- <mr-widget-merge-help />
- </div>
</div>
- `,
-};
+ <div
+ class="mr-widget-footer"
+ v-if="shouldRenderMergeHelp"
+ >
+ <mr-widget-merge-help />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index a47ca9fae86..83b7b054e6f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -20,6 +20,7 @@ export default class MergeRequestStore {
this.sourceBranch = data.source_branch;
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
+ this.shortMergeCommitSha = data.short_merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
@@ -65,6 +66,7 @@ export default class MergeRequestStore {
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path;
+ this.mergeCommitPath = data.merge_commit_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
diff --git a/app/assets/javascripts/vue_shared/components/callout.vue b/app/assets/javascripts/vue_shared/components/callout.vue
new file mode 100644
index 00000000000..ccf802c456c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/callout.vue
@@ -0,0 +1,27 @@
+<script>
+const calloutVariants = ['danger', 'success', 'info', 'warning'];
+
+export default {
+ props: {
+ category: {
+ type: String,
+ required: false,
+ default: calloutVariants[0],
+ validator: value => calloutVariants.includes(value),
+ },
+ message: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div
+ :class="`bs-callout bs-callout-${category}`"
+ role="alert"
+ aria-live="assertive"
+ >
+ {{ message }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 5324d5dc797..0d64efcbf68 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,52 +1,52 @@
<script>
- import ciIcon from './ci_icon.vue';
- import tooltip from '../directives/tooltip';
- /**
- * Renders CI Badge link with CI icon and status text based on
- * API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table - first column
- * - Jobs table - first column
- * - Pipeline show view - header
- * - Job show view - header
- * - MR widget
- */
+import CiIcon from './ci_icon.vue';
+import tooltip from '../directives/tooltip';
+/**
+ * Renders CI Badge link with CI icon and status text based on
+ * API response shared between all places where it is used.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table - first column
+ * - Jobs table - first column
+ * - Pipeline show view - header
+ * - Job show view - header
+ * - MR widget
+ */
- export default {
- components: {
- ciIcon,
+export default {
+ components: {
+ CiIcon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ showText: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
- showText: {
- type: Boolean,
- required: false,
- default: true,
- },
+ },
+ computed: {
+ cssClass() {
+ const className = this.status.group;
+ return className ? `ci-status ci-${className}` : 'ci-status';
},
- computed: {
- cssClass() {
- const className = this.status.group;
- return className ? `ci-status ci-${className}` : 'ci-status';
- },
- },
- };
+ },
+};
</script>
<template>
<a
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 8fea746f4de..fcab8f571dd 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,45 +1,44 @@
<script>
- import icon from '../../vue_shared/components/icon.vue';
+import Icon from '../../vue_shared/components/icon.vue';
- /**
- * Renders CI icon based on API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table Badge
- * - Pipelines table mini graph
- * - Pipeline graph
- * - Pipeline show view badge
- * - Jobs table
- * - Jobs show view header
- * - Jobs show view sidebar
- */
- export default {
- components: {
- icon,
+/**
+ * Renders CI icon based on API response shared between all places where it is used.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table Badge
+ * - Pipelines table mini graph
+ * - Pipeline graph
+ * - Pipeline show view badge
+ * - Jobs table
+ * - Jobs show view header
+ * - Jobs show view sidebar
+ */
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ cssClass() {
+ const status = this.status.group;
+ return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
-
- computed: {
- cssClass() {
- const status = this.status.group;
- return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
- },
- },
- };
+ },
+};
</script>
<template>
<span :class="cssClass">
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index cab126a7eca..cb2cc3901ad 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -1,40 +1,50 @@
<script>
- /**
- * Falls back to the code used in `copy_to_clipboard.js`
- */
- import tooltip from '../directives/tooltip';
+/**
+ * Falls back to the code used in `copy_to_clipboard.js`
+ *
+ * Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
+ * when clicked.
+ *
+ * @example
+ * <clipboard-button
+ * title="Copy to clipbard"
+ * text="Content to be copied"
+ * css-class="btn-transparent"
+ * />
+ */
+import tooltip from '../directives/tooltip';
- export default {
- name: 'ClipboardButton',
- directives: {
- tooltip,
+export default {
+ name: 'ClipboardButton',
+ directives: {
+ tooltip,
+ },
+ props: {
+ text: {
+ type: String,
+ required: true,
},
- props: {
- text: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- tooltipContainer: {
- type: [String, Boolean],
- required: false,
- default: false,
- },
- cssClass: {
- type: String,
- required: false,
- default: 'btn-default',
- },
+ title: {
+ type: String,
+ required: true,
},
- };
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ tooltipContainer: {
+ type: [String, Boolean],
+ required: false,
+ default: false,
+ },
+ cssClass: {
+ type: String,
+ required: false,
+ default: 'btn-default',
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index b8875d04488..8f250a6c989 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,119 +1,111 @@
<script>
- import commitIconSvg from 'icons/_icon_commit.svg';
- import userAvatarLink from './user_avatar/user_avatar_link.vue';
- import tooltip from '../directives/tooltip';
- import icon from '../../vue_shared/components/icon.vue';
+import UserAvatarLink from './user_avatar/user_avatar_link.vue';
+import tooltip from '../directives/tooltip';
+import Icon from '../../vue_shared/components/icon.vue';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ UserAvatarLink,
+ Icon,
+ },
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render a svg sprite fork icon
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- components: {
- userAvatarLink,
- icon,
+ /**
+ * 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: '',
},
- props: {
- /**
- * Indicates the existance of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
- * if false will render a svg sprite 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: () => ({}),
- },
- showBranch: {
- type: Boolean,
- required: false,
- default: true,
- },
+ /**
+ * 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: () => ({}),
+ },
+ showBranch: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- computed: {
- /**
- * Used to verify if all the properties needed to render the commit
- * ref section were provided.
- *
- * @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.
- *
- * @returns {Boolean}
- */
- hasAuthor() {
- return this.author &&
- this.author.avatar_url &&
- this.author.path &&
- 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;
- },
+ },
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
- created() {
- this.commitIconSvg = commitIconSvg;
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author && this.author.avatar_url && this.author.path && 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;
+ },
+ },
+};
</script>
<template>
<div class="branch-commit">
@@ -141,11 +133,10 @@
{{ commitRef.name }}
</a>
</template>
- <div
- v-html="commitIconSvg"
+ <icon
+ name="commit"
class="commit-icon js-commit-icon"
- >
- </div>
+ />
<a
class="commit-sha"
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index 395a71acccf..7b5367ac19b 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -42,7 +42,7 @@ export default {
target="_blank">
<icon
name="download"
- css-classes="pull-left append-right-8"
+ css-classes="float-left append-right-8"
:size="16"
/>
{{ __('Download') }}
diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
index dcf1489b37c..424af5a0293 100644
--- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
@@ -87,7 +87,7 @@
<div
:id="id"
class="modal"
- :class="id ? '' : 'show'"
+ :class="id ? '' : 'd-block'"
role="dialog"
tabindex="-1"
>
@@ -99,12 +99,12 @@
<div class="modal-content">
<div class="modal-header">
<slot name="header">
- <h4 class="modal-title pull-left">
+ <h4 class="modal-title float-left">
{{ title }}
</h4>
<button
type="button"
- class="close pull-right"
+ class="close float-right"
@click="emitCancel($event)"
data-dismiss="modal"
aria-label="Close"
@@ -166,7 +166,7 @@
</div>
<div
v-if="!id"
- class="modal-backdrop fade in"
+ class="modal-backdrop fade show"
>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index c943c8d98a4..9295be3e2b2 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -1,33 +1,33 @@
<script>
- import { __ } from '~/locale';
- /**
- * Port of detail_behavior expand button.
- *
- * @example
- * <expand-button>
- * <template slot="expanded">
- * Text goes here.
- * </template>
- * </expand-button>
- */
- export default {
- name: 'ExpandButton',
- data() {
- return {
- isCollapsed: true,
- };
+import { __ } from '~/locale';
+/**
+ * Port of detail_behavior expand button.
+ *
+ * @example
+ * <expand-button>
+ * <template slot="expanded">
+ * Text goes here.
+ * </template>
+ * </expand-button>
+ */
+export default {
+ name: 'ExpandButton',
+ data() {
+ return {
+ isCollapsed: true,
+ };
+ },
+ computed: {
+ ariaLabel() {
+ return __('Click to expand text');
},
- computed: {
- ariaLabel() {
- return __('Click to expand text');
- },
+ },
+ methods: {
+ onClick() {
+ this.isCollapsed = !this.isCollapsed;
},
- methods: {
- onClick() {
- this.isCollapsed = !this.isCollapsed;
- },
- },
- };
+ },
+};
</script>
<template>
<span>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index ee1c3498748..be2755452e2 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,9 +1,9 @@
<script>
- import getIconForFile from './file_icon/file_icon_map';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
+import getIconForFile from './file_icon/file_icon_map';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import icon from '../../vue_shared/components/icon.vue';
- /* This is a re-usable vue component for rendering a svg sprite
+/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
@@ -15,60 +15,60 @@
/>
*/
- export default {
- components: {
- loadingIcon,
- icon,
+export default {
+ components: {
+ loadingIcon,
+ icon,
+ },
+ props: {
+ fileName: {
+ type: String,
+ required: true,
},
- props: {
- fileName: {
- type: String,
- required: true,
- },
- folder: {
- type: Boolean,
- required: false,
- default: false,
- },
+ folder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- opened: {
- type: Boolean,
- required: false,
- default: false,
- },
+ opened: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- size: {
- type: Number,
- required: false,
- default: 16,
- },
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ spriteHref() {
+ const iconName = getIconForFile(this.fileName) || 'file';
+ return `${gon.sprite_file_icons}#${iconName}`;
+ },
+ folderIconName() {
+ return this.opened ? 'folder-open' : 'folder';
},
- computed: {
- spriteHref() {
- const iconName = getIconForFile(this.fileName) || 'file';
- return `${gon.sprite_file_icons}#${iconName}`;
- },
- folderIconName() {
- return this.opened ? 'folder-open' : 'folder';
- },
- iconSizeClass() {
- return this.size ? `s${this.size}` : '';
- },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
},
- };
+ },
+};
</script>
<template>
<span>
@@ -82,6 +82,7 @@
v-if="!loading && folder"
:name="folderIconName"
:size="size"
+ css-classes="folder-icon"
/>
<loading-icon
v-if="loading"
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue
index f28e5e2715d..d5d5a7d3798 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue
@@ -53,6 +53,11 @@ export default {
<div class="modal-content">
<div class="modal-header">
<slot name="header">
+ <h4 class="modal-title">
+ <slot name="title">
+ {{ headerTitleText }}
+ </slot>
+ </h4>
<button
type="button"
class="close js-modal-close-action"
@@ -62,11 +67,6 @@ export default {
>
<span aria-hidden="true">&times;</span>
</button>
- <h4 class="modal-title">
- <slot name="title">
- {{ headerTitleText }}
- </slot>
- </h4>
</slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index a0cd0cbd200..ca17fa06a00 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,78 +1,78 @@
<script>
- import ciIconBadge from './ci_badge_link.vue';
- import loadingIcon from './loading_icon.vue';
- import timeagoTooltip from './time_ago_tooltip.vue';
- import tooltip from '../directives/tooltip';
- import userAvatarImage from './user_avatar/user_avatar_image.vue';
+import CiIconBadge from './ci_badge_link.vue';
+import LoadingIcon from './loading_icon.vue';
+import TimeagoTooltip from './time_ago_tooltip.vue';
+import tooltip from '../directives/tooltip';
+import UserAvatarImage from './user_avatar/user_avatar_image.vue';
- /**
- * Renders header component for job and pipeline page based on UI mockups
- *
- * Used in:
- * - job show page
- * - pipeline show page
- */
- export default {
- components: {
- ciIconBadge,
- loadingIcon,
- timeagoTooltip,
- userAvatarImage,
+/**
+ * Renders header component for job and pipeline page based on UI mockups
+ *
+ * Used in:
+ * - job show page
+ * - pipeline show page
+ */
+export default {
+ components: {
+ CiIconBadge,
+ LoadingIcon,
+ TimeagoTooltip,
+ UserAvatarImage,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ itemName: {
+ type: String,
+ required: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
- itemName: {
- type: String,
- required: true,
- },
- itemId: {
- type: Number,
- required: true,
- },
- time: {
- type: String,
- required: true,
- },
- user: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- actions: {
- type: Array,
- required: false,
- default: () => [],
- },
- hasSidebarButton: {
- type: Boolean,
- required: false,
- default: false,
- },
- shouldRenderTriggeredLabel: {
- type: Boolean,
- required: false,
- default: true,
- },
+ itemId: {
+ type: Number,
+ required: true,
},
+ time: {
+ type: String,
+ required: true,
+ },
+ user: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ hasSidebarButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ shouldRenderTriggeredLabel: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
- computed: {
- userAvatarAltText() {
- return `${this.user.name}'s avatar`;
- },
+ computed: {
+ userAvatarAltText() {
+ return `${this.user.name}'s avatar`;
},
+ },
- methods: {
- onClickAction(action) {
- this.$emit('actionClicked', action);
- },
+ methods: {
+ onClickAction(action) {
+ this.$emit('actionClicked', action);
},
- };
+ },
+};
</script>
<template>
@@ -163,8 +163,8 @@
<button
v-if="hasSidebarButton"
type="button"
- class="btn btn-default visible-xs-block
-visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
+ class="btn btn-default d-block d-sm-none d-md-none
+sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-label="Toggle Sidebar"
id="toggleSidebar"
>
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 6a2e05000e1..c42c4a1fbe7 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -1,85 +1,88 @@
<script>
+/* This is a re-usable vue component for rendering a svg sprite
+ icon
- /* This is a re-usable vue component for rendering a svg sprite
- icon
+ Sample configuration:
- Sample configuration:
+ <icon
+ name="retry"
+ :size="32"
+ css-classes="top"
+ />
- <icon
- name="retry"
- :size="32"
- css-classes="top"
- />
+*/
+// only allow classes in images.scss e.g. s12
+const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
- */
- // only allow classes in images.scss e.g. s12
- const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
-
- export default {
- props: {
- name: {
- type: String,
- required: true,
- },
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
- size: {
- type: Number,
- required: false,
- default: 16,
- validator(value) {
- return validSizes.includes(value);
- },
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ validator(value) {
+ return validSizes.includes(value);
},
+ },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
- width: {
- type: Number,
- required: false,
- default: null,
- },
+ width: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- height: {
- type: Number,
- required: false,
- default: null,
- },
+ height: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- y: {
- type: Number,
- required: false,
- default: null,
- },
+ y: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- x: {
- type: Number,
- required: false,
- default: null,
- },
+ x: {
+ type: Number,
+ required: false,
+ default: null,
},
+ },
- computed: {
- spriteHref() {
- return `${gon.sprite_icons}#${this.name}`;
- },
- iconSizeClass() {
- return this.size ? `s${this.size}` : '';
- },
+ computed: {
+ spriteHref() {
+ return `${gon.sprite_icons}#${this.name}`;
+ },
+ iconTestClass() {
+ return `ic-${this.name}`;
+ },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
},
- };
+ },
+};
</script>
<template>
<svg
- :class="[iconSizeClass, cssClasses]"
+ :class="[iconSizeClass, iconTestClass, cssClasses]"
:width="width"
:height="height"
:x="x"
- :y="y">
+ :y="y"
+ >
<use v-bind="{ 'xlink:href':spriteHref }" />
</svg>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue
index 0a30f467b08..23010f40f26 100644
--- a/app/assets/javascripts/vue_shared/components/identicon.vue
+++ b/app/assets/javascripts/vue_shared/components/identicon.vue
@@ -17,7 +17,7 @@ export default {
},
computed: {
/**
- * This method is based on app/helpers/application_helper.rb#project_identicon
+ * This method is based on app/helpers/avatars_helper.rb#project_identicon
*/
identiconStyles() {
const allowedColors = [
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index e832d94d32f..88c13a1f340 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -70,12 +70,14 @@
/>
</transition>
<transition name="fade">
- <span
- v-if="label"
- class="js-loading-button-label"
- >
- {{ label }}
- </span>
+ <slot>
+ <span
+ v-if="label"
+ class="js-loading-button-label"
+ >
+ {{ label }}
+ </span>
+ </slot>
</transition>
</button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index c0ee88bbf72..d63318f3da6 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -111,7 +111,7 @@
Attach a file
</button>
<button
- class="btn btn-default btn-xs hide button-cancel-uploading-files"
+ class="btn btn-default btn-sm hide button-cancel-uploading-files"
type="button"
>
Cancel
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index b33a0101dbf..08d4936f480 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -1,53 +1,53 @@
<script>
- import $ from 'jquery';
+import $ from 'jquery';
- /**
- * Given an array of tabs, renders non linked bootstrap tabs.
- * When a tab is clicked it will trigger an event and provide the clicked scope.
- *
- * This component is used in apps that handle the API call.
- * If you only need to change the URL this component should not be used.
- *
- * @example
- * <navigation-tabs
- * :tabs="[
- * {
- * name: String,
- * scope: String,
- * count: Number || Undefined,
- * isActive: Boolean,
- * },
- * ]"
- * @onChangeTab="onChangeTab"
- * />
- */
- export default {
- name: 'NavigationTabs',
- props: {
- tabs: {
- type: Array,
- required: true,
- },
- scope: {
- type: String,
- required: false,
- default: '',
- },
+/**
+ * Given an array of tabs, renders non linked bootstrap tabs.
+ * When a tab is clicked it will trigger an event and provide the clicked scope.
+ *
+ * This component is used in apps that handle the API call.
+ * If you only need to change the URL this component should not be used.
+ *
+ * @example
+ * <navigation-tabs
+ * :tabs="[
+ * {
+ * name: String,
+ * scope: String,
+ * count: Number || Undefined || Null,
+ * isActive: Boolean,
+ * },
+ * ]"
+ * @onChangeTab="onChangeTab"
+ * />
+ */
+export default {
+ name: 'NavigationTabs',
+ props: {
+ tabs: {
+ type: Array,
+ required: true,
},
- mounted() {
- $(document).trigger('init.scrolling-tabs');
+ scope: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ mounted() {
+ $(document).trigger('init.scrolling-tabs');
+ },
+ methods: {
+ shouldRenderBadge(count) {
+ // 0 is valid in a badge, but evaluates to false, we need to check for undefined or null
+ return !(count === undefined || count === null);
},
- methods: {
- shouldRenderBadge(count) {
- // 0 is valid in a badge, but evaluates to false, we need to check for undefined
- return count !== undefined;
- },
- onTabClick(tab) {
- this.$emit('onChangeTab', tab.scope);
- },
+ onTabClick(tab) {
+ this.$emit('onChangeTab', tab.scope);
},
- };
+ },
+};
</script>
<template>
<ul class="nav-links scrolling-tabs separator">
@@ -67,7 +67,7 @@
<span
v-if="shouldRenderBadge(tab.count)"
- class="badge"
+ class="badge badge-pill"
>
{{ tab.count }}
</span>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index 50b1508691b..eccba61a8c0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -54,7 +54,7 @@
<div class="note-header">
<div class="note-header-info">
<a :href="getUserData.path">
- <span class="hidden-xs">{{ getUserData.name }}</span>
+ <span class="d-none d-sm-block">{{ getUserData.name }}</span>
<span class="note-headline-light">@{{ getUserData.username }}</span>
</a>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 3fcacd156c5..71ec34f2c7a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -116,7 +116,7 @@
v-if="isLoading"
:inline="true"
/>
- <div class="pull-right">
+ <div class="float-right">
<button
v-if="editable && !editing"
type="button"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 5ede53d8d01..70b46a9c2bb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -1,4 +1,5 @@
<script>
+import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue';
@@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
+ $(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
},
methods: {
handleClick(label) {
this.$emit('onLabelClick', label);
},
+ handleCollapsedValueClick() {
+ this.$emit('toggleCollapse');
+ },
+ handleDropdownHidden() {
+ this.$emit('onDropdownClose');
+ },
},
};
</script>
@@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
+ @onValueClick="handleCollapsedValueClick"
/>
<dropdown-title
:can-edit="canEdit"
@@ -133,7 +142,10 @@ export default {
:name="hiddenInputName"
:label="label"
/>
- <div class="dropdown">
+ <div
+ class="dropdown"
+ ref="dropdown"
+ >
<dropdown-button
:ability-name="abilityName"
:field-name="hiddenInputName"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
index 34a07f33a23..3c400afdc1d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
@@ -77,13 +77,13 @@ export default {
<div class="clearfix">
<button
type="button"
- class="btn btn-primary pull-left js-new-label-btn disabled"
+ class="btn btn-primary float-left js-new-label-btn disabled"
>
{{ __('Create') }}
</button>
<button
type="button"
- class="btn btn-default pull-right js-cancel-label-btn"
+ class="btn btn-default float-right js-cancel-label-btn"
>
{{ __('Cancel') }}
</button>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
index 7da82e90e29..9ac32ff13c6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
@@ -21,7 +21,7 @@ export default {
</i>
<button
type="button"
- class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle"
+ class="edit-link btn btn-blank float-right js-sidebar-dropdown-toggle"
>
{{ __('Edit') }}
</button>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index 5cf728fe050..68fa2ab8d01 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -26,6 +26,11 @@ export default {
return labelsString;
},
},
+ methods: {
+ handleClick() {
+ this.$emit('onValueClick');
+ },
+ },
};
</script>
@@ -36,6 +41,7 @@ export default {
data-placement="left"
data-container="body"
:title="labelsList"
+ @click="handleClick"
>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
index 8211d425b1f..de6f8c32e74 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -1,18 +1,29 @@
<script>
- export default {
- name: 'ToggleSidebar',
- props: {
- collapsed: {
- type: Boolean,
- required: true,
- },
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ name: 'ToggleSidebar',
+ directives: {
+ tooltip,
+ },
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ tooltipLabel() {
+ return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar');
},
- methods: {
- toggle() {
- this.$emit('toggle');
- },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
},
- };
+ },
+};
</script>
<template>
@@ -20,6 +31,10 @@
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<i
aria-label="toggle collapse"
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index bec4e7c99b6..368eeb6c453 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -40,7 +40,7 @@ export default {
:class="cssClass"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
- data-container="body">
- {{ timeFormated(time) }}
+ data-container="body"
+ v-text="timeFormated(time)">
</time>
</template>
diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js
index eb35294906b..c913bc34c68 100644
--- a/app/assets/javascripts/vue_shared/directives/popover.js
+++ b/app/assets/javascripts/vue_shared/directives/popover.js
@@ -17,6 +17,6 @@ export default {
},
unbind(el) {
- $(el).popover('destroy');
+ $(el).popover('dispose');
},
};
diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js
index b7f7e9fec15..155e6b6698a 100644
--- a/app/assets/javascripts/vue_shared/directives/tooltip.js
+++ b/app/assets/javascripts/vue_shared/directives/tooltip.js
@@ -6,10 +6,10 @@ export default {
},
componentUpdated(el) {
- $(el).tooltip('fixTitle');
+ $(el).tooltip('_fixTitle');
},
unbind(el) {
- $(el).tooltip('destroy');
+ $(el).tooltip('dispose');
},
};
diff --git a/app/assets/javascripts/vue_shared/models/label.js b/app/assets/javascripts/vue_shared/models/label.js
index 70b9efe0c68..d29c7fe973a 100644
--- a/app/assets/javascripts/vue_shared/models/label.js
+++ b/app/assets/javascripts/vue_shared/models/label.js
@@ -1,4 +1,4 @@
-class ListLabel {
+export default class ListLabel {
constructor(obj) {
this.id = obj.id;
this.title = obj.title;