summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/images/cluster_app_logos/knative.pngbin0 -> 11259 bytes
-rw-r--r--app/assets/javascripts/badges/components/badge.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js2
-rw-r--r--app/assets/javascripts/boards/components/board.js27
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue128
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js41
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue362
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue90
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue48
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue16
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue80
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue231
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue198
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue30
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js43
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue8
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue122
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js9
-rw-r--r--app/assets/javascripts/boards/index.js6
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js10
-rw-r--r--app/assets/javascripts/boards/models/issue.js32
-rw-r--r--app/assets/javascripts/boards/models/list.js4
-rw-r--r--app/assets/javascripts/boards/services/board_service.js12
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js57
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js12
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js15
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js24
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue72
-rw-r--r--app/assets/javascripts/clusters/constants.js8
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js13
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue19
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js17
-rw-r--r--app/assets/javascripts/commons/index.js1
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue4
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue7
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue72
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue55
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue139
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue2
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue10
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue4
-rw-r--r--app/assets/javascripts/diffs/constants.js1
-rw-r--r--app/assets/javascripts/diffs/store/actions.js19
-rw-r--r--app/assets/javascripts/diffs/store/getters.js5
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js53
-rw-r--r--app/assets/javascripts/diffs/store/utils.js20
-rw-r--r--app/assets/javascripts/dirty_submit/dirty_submit_form.js3
-rw-r--r--app/assets/javascripts/environments/components/container.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue42
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue9
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue61
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue8
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue17
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue7
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue152
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue2
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue6
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js30
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js7
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js27
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js113
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js14
-rw-r--r--app/assets/javascripts/flash.js8
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js63
-rw-r--r--app/assets/javascripts/gl_field_errors.js2
-rw-r--r--app/assets/javascripts/groups/components/app.vue2
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue4
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue4
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue2
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue2
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue4
-rw-r--r--app/assets/javascripts/ide/stores/getters.js2
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue14
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue10
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue9
-rw-r--r--app/assets/javascripts/jobs/components/erased_block.vue6
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue334
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue21
-rw-r--r--app/assets/javascripts/jobs/components/job_log.vue74
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue44
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue31
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue9
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue8
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue10
-rw-r--r--app/assets/javascripts/jobs/index.js1
-rw-r--r--app/assets/javascripts/jobs/mixins/delayed_job_mixin.js50
-rw-r--r--app/assets/javascripts/jobs/store/getters.js9
-rw-r--r--app/assets/javascripts/labels_select.js248
-rw-r--r--app/assets/javascripts/lib/utils/ace_utils.js4
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js12
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js2
-rw-r--r--app/assets/javascripts/members.js10
-rw-r--r--app/assets/javascripts/merge_request_tabs.js3
-rw-r--r--app/assets/javascripts/milestone_select.js5
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue12
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue36
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue30
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue23
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue71
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue29
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue12
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue167
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue11
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue16
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue94
-rw-r--r--app/assets/javascripts/notes/constants.js2
-rw-r--r--app/assets/javascripts/notes/discussion_filters.js12
-rw-r--r--app/assets/javascripts/notes/stores/actions.js4
-rw-r--r--app/assets/javascripts/notes/stores/collapse_utils.js2
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/javascripts/pages/groups/clusters/destroy/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/show/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/update/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/index.js16
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue99
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/new/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/project.js4
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue348
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue21
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue43
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue38
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue23
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js40
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js2
-rw-r--r--app/assets/javascripts/projects/project_new.js44
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/registry/components/app.vue2
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue2
-rw-r--r--app/assets/javascripts/reports/components/issues_list.vue85
-rw-r--r--app/assets/javascripts/reports/components/report_item.vue (renamed from app/assets/javascripts/reports/components/report_issues.vue)44
-rw-r--r--app/assets/javascripts/reports/components/summary_row.vue2
-rw-r--r--app/assets/javascripts/right_sidebar.js57
-rw-r--r--app/assets/javascripts/search_autocomplete.js4
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue124
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue2
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js20
-rw-r--r--app/assets/javascripts/users_select.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-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_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue32
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue83
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue79
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_countdown.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_links.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue62
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue141
-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.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/smart_virtual_list.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue53
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue14
-rw-r--r--app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js9
-rw-r--r--app/assets/stylesheets/framework/awards.scss5
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss42
-rw-r--r--app/assets/stylesheets/framework/common.scss7
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss5
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss4
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/tables.scss6
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss10
-rw-r--r--app/assets/stylesheets/pages/boards.scss197
-rw-r--r--app/assets/stylesheets/pages/builds.scss22
-rw-r--r--app/assets/stylesheets/pages/clusters.scss35
-rw-r--r--app/assets/stylesheets/pages/diff.scss50
-rw-r--r--app/assets/stylesheets/pages/environments.scss7
-rw-r--r--app/assets/stylesheets/pages/events.scss78
-rw-r--r--app/assets/stylesheets/pages/note_form.scss14
-rw-r--r--app/assets/stylesheets/pages/notes.scss294
-rw-r--r--app/assets/stylesheets/pages/pages.scss7
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss8
-rw-r--r--app/assets/stylesheets/pages/profile.scss12
247 files changed, 4387 insertions, 2683 deletions
diff --git a/app/assets/images/cluster_app_logos/knative.png b/app/assets/images/cluster_app_logos/knative.png
new file mode 100644
index 00000000000..0a2510c8549
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/knative.png
Binary files differ
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 97232d7f783..8512bf9dd7b 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -1,12 +1,14 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
name: 'Badge',
components: {
Icon,
Tooltip,
+ GlLoadingIcon,
},
directives: {
Tooltip,
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index aff7c4180e3..47e6e618219 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -4,6 +4,7 @@ import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
@@ -14,6 +15,7 @@ export default {
components: {
Badge,
LoadingButton,
+ GlLoadingIcon,
},
props: {
isEditing: {
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index 359d3e10380..ab518820378 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -1,5 +1,6 @@
<script>
import { mapState } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import BadgeListRow from './badge_list_row.vue';
import { GROUP_BADGE } from '../constants';
@@ -7,6 +8,7 @@ export default {
name: 'BadgeList',
components: {
BadgeListRow,
+ GlLoadingIcon,
},
computed: {
...mapState(['badges', 'isLoading', 'kind']),
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index 5d16ba3ce6d..f28eff18f03 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -2,6 +2,7 @@
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
@@ -10,6 +11,7 @@ export default {
components: {
Badge,
Icon,
+ GlLoadingIcon,
},
props: {
badge: {
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 3cc89ff1955..ec27ae8c291 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -13,7 +13,7 @@ export default () => {
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot');
const assetsPath = editBlobForm.data('assetsPrefix');
- const filePath = editBlobForm.data('blobFilename')
+ const filePath = editBlobForm.data('blobFilename');
const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id');
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 623cda5679a..fb6e5291a61 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -42,7 +42,7 @@ export default Vue.extend({
required: true,
},
},
- data () {
+ data() {
return {
detailIssue: boardsStore.detail,
filter: boardsStore.filter,
@@ -55,27 +55,26 @@ export default Vue.extend({
},
isNewIssueShown() {
return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed');
- }
+ },
},
watch: {
filter: {
handler() {
this.list.page = 1;
- this.list.getIssues(true)
- .catch(() => {
- // TODO: handle request error
- });
+ this.list.getIssues(true).catch(() => {
+ // TODO: handle request error
+ });
},
deep: true,
- }
+ },
},
- mounted () {
+ mounted() {
this.sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
handle: '.js-board-handle',
- onEnd: (e) => {
+ onEnd: e => {
sortableEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
@@ -86,14 +85,15 @@ export default Vue.extend({
boardsStore.moveList(list, order);
});
}
- }
+ },
});
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
},
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
- const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
+ const isCollapsed =
+ localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed;
}
@@ -107,7 +107,10 @@ export default Vue.extend({
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
- localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
+ localStorage.setItem(
+ `boards.${this.boardId}.${this.list.type}.expanded`,
+ this.list.isExpanded,
+ );
}
}
},
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index 38aaec73d7d..561a4636ef5 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -32,18 +32,18 @@ export default {
boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position');
// Save the labels
- gl.boardService.generateDefaultLists()
+ gl.boardService
+ .generateDefaultLists()
.then(res => res.data)
- .then((data) => {
- data.forEach((listObj) => {
+ .then(data => {
+ data.forEach(listObj => {
const list = boardsStore.findList('title', listObj.title);
list.id = listObj.id;
list.label.id = listObj.label.id;
- list.getIssues()
- .catch(() => {
- // TODO: handle request error
- });
+ list.getIssues().catch(() => {
+ // TODO: handle request error
+ });
});
})
.catch(() => {
@@ -57,7 +57,6 @@ export default {
clearBlankState: boardsStore.removeBlankState.bind(boardsStore),
},
};
-
</script>
<template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 843498f0d06..2f31316aa76 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,77 +1,77 @@
<script>
- /* eslint-disable vue/require-default-prop */
- import IssueCardInner from './issue_card_inner.vue';
- import eventHub from '../eventhub';
- import boardsStore from '../stores/boards_store';
+/* eslint-disable vue/require-default-prop */
+import IssueCardInner from './issue_card_inner.vue';
+import eventHub from '../eventhub';
+import boardsStore from '../stores/boards_store';
- export default {
- name: 'BoardsIssueCard',
- components: {
- IssueCardInner,
+export default {
+ name: 'BoardsIssueCard',
+ components: {
+ IssueCardInner,
+ },
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
},
- props: {
- list: {
- type: Object,
- default: () => ({}),
- },
- issue: {
- type: Object,
- default: () => ({}),
- },
- issueLinkBase: {
- type: String,
- default: '',
- },
- disabled: {
- type: Boolean,
- default: false,
- },
- index: {
- type: Number,
- default: 0,
- },
- rootPath: {
- type: String,
- default: '',
- },
- groupId: {
- type: Number,
- },
+ issue: {
+ type: Object,
+ default: () => ({}),
},
- data() {
- return {
- showDetail: false,
- detailIssue: boardsStore.detail,
- };
+ issueLinkBase: {
+ type: String,
+ default: '',
},
- computed: {
- issueDetailVisible() {
- return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
- },
+ disabled: {
+ type: Boolean,
+ default: false,
},
- methods: {
- mouseDown() {
- this.showDetail = true;
- },
- mouseMove() {
- this.showDetail = false;
- },
- showIssue(e) {
- if (e.target.classList.contains('js-no-trigger')) return;
+ index: {
+ type: Number,
+ default: 0,
+ },
+ rootPath: {
+ type: String,
+ default: '',
+ },
+ groupId: {
+ type: Number,
+ },
+ },
+ data() {
+ return {
+ showDetail: false,
+ detailIssue: boardsStore.detail,
+ };
+ },
+ computed: {
+ issueDetailVisible() {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ },
+ },
+ methods: {
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ if (e.target.classList.contains('js-no-trigger')) return;
- if (this.showDetail) {
- this.showDetail = false;
+ if (this.showDetail) {
+ this.showDetail = false;
- if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
- eventHub.$emit('clearDetailIssue');
- } else {
- eventHub.$emit('newDetailIssue', this.issue);
- boardsStore.detail.list = this.list;
- }
+ if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
+ eventHub.$emit('clearDetailIssue');
+ } else {
+ eventHub.$emit('newDetailIssue', this.issue);
+ boardsStore.detail.list = this.list;
}
- },
+ }
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 4dc56c670f0..5e28fc396ab 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,5 +1,6 @@
<script>
import Sortable from 'sortablejs';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
@@ -11,6 +12,7 @@ export default {
components: {
boardCard,
boardNewIssue,
+ GlLoadingIcon,
},
props: {
groupId: {
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index ae2d1ee3c6e..ee3dc38bca6 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -62,7 +62,8 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
- return this.list.newIssue(issue)
+ return this.list
+ .newIssue(issue)
.then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 62666954de0..e637e1f1223 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -38,7 +38,7 @@ export default Vue.extend({
};
},
computed: {
- showSidebar () {
+ showSidebar() {
return Object.keys(this.issue).length;
},
milestoneTitle() {
@@ -51,18 +51,20 @@ export default Vue.extend({
return this.issue.labels && this.issue.labels.length;
},
labelDropdownTitle() {
- return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
- firstLabel: this.issue.labels[0].title,
- labelCount: this.issue.labels.length - 1
- }) : __('Label');
+ return this.hasLabels
+ ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
+ firstLabel: this.issue.labels[0].title,
+ labelCount: this.issue.labels.length - 1,
+ })
+ : __('Label');
},
selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
- }
+ },
},
watch: {
detail: {
- handler () {
+ handler() {
if (this.issue.id !== this.detail.issue.id) {
$('.block.assignee')
.find('input:not(.js-vue)[name="issue[assignee_ids][]"]')
@@ -71,17 +73,19 @@ export default Vue.extend({
});
$('.js-issue-board-sidebar', this.$el).each((i, el) => {
- $(el).data('glDropdown').clearMenu();
+ $(el)
+ .data('glDropdown')
+ .clearMenu();
});
}
this.issue = this.detail.issue;
this.list = this.detail.list;
},
- deep: true
+ deep: true,
},
},
- created () {
+ created() {
// Get events from glDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
@@ -94,7 +98,7 @@ export default Vue.extend({
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
},
- mounted () {
+ mounted() {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new DueDateSelectors();
@@ -102,29 +106,30 @@ export default Vue.extend({
new Sidebar();
},
methods: {
- closeSidebar () {
+ closeSidebar() {
this.detail.issue = {};
},
- assignSelf () {
+ assignSelf() {
// Notify gl dropdown that we are now assigning to current user
this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself'));
this.addAssignee(this.currentUser);
this.saveAssignees();
},
- removeAssignee (a) {
+ removeAssignee(a) {
boardsStore.detail.issue.removeAssignee(a);
},
- addAssignee (a) {
+ addAssignee(a) {
boardsStore.detail.issue.addAssignee(a);
},
- removeAllAssignees () {
+ removeAllAssignees() {
boardsStore.detail.issue.removeAllAssignees();
},
- saveAssignees () {
+ saveAssignees() {
this.loadingAssignees = true;
- boardsStore.detail.issue.update()
+ boardsStore.detail.issue
+ .update()
.then(() => {
this.loadingAssignees = false;
})
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index aa98f35786e..2315a48a306 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,164 +1,222 @@
<script>
- import $ from 'jquery';
- import Icon from '~/vue_shared/components/icon.vue';
- import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
- import eventHub from '../eventhub';
- import tooltip from '../../vue_shared/directives/tooltip';
- import boardsStore from '../stores/boards_store';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import { sprintf, __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import eventHub from '../eventhub';
+import IssueDueDate from './issue_due_date.vue';
+import IssueTimeEstimate from './issue_time_estimate.vue';
+import boardsStore from '../stores/boards_store';
- export default {
- components: {
- UserAvatarLink,
- Icon,
- },
- directives: {
- tooltip,
- },
- props: {
- issue: {
- type: Object,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- list: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- rootPath: {
- type: String,
- required: true,
- },
- updateFilters: {
- type: Boolean,
- required: false,
- default: false,
- },
- groupId: {
- type: Number,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- limitBeforeCounter: 3,
- maxRender: 4,
- maxCounter: 99,
- };
+export default {
+ components: {
+ Icon,
+ UserAvatarLink,
+ TooltipOnTruncate,
+ IssueDueDate,
+ IssueTimeEstimate,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ issue: {
+ type: Object,
+ required: true,
},
- computed: {
- numberOverLimit() {
- return this.issue.assignees.length - this.limitBeforeCounter;
- },
- assigneeCounterTooltip() {
- return `${this.assigneeCounterLabel} more`;
- },
- assigneeCounterLabel() {
- if (this.numberOverLimit > this.maxCounter) {
- return `${this.maxCounter}+`;
- }
-
- return `+${this.numberOverLimit}`;
- },
- shouldRenderCounter() {
- if (this.issue.assignees.length <= this.maxRender) {
- return false;
- }
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ updateFilters: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ groupId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ limitBeforeCounter: 2,
+ maxRender: 3,
+ maxCounter: 99,
+ };
+ },
+ computed: {
+ numberOverLimit() {
+ return this.issue.assignees.length - this.limitBeforeCounter;
+ },
+ assigneeCounterTooltip() {
+ const { numberOverLimit, maxCounter } = this;
+ const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit;
+ return sprintf(__('%{count} more assignees'), { count });
+ },
+ assigneeCounterLabel() {
+ if (this.numberOverLimit > this.maxCounter) {
+ return `${this.maxCounter}+`;
+ }
- return this.issue.assignees.length > this.numberOverLimit;
- },
- issueId() {
- if (this.issue.iid) {
- return `#${this.issue.iid}`;
- }
+ return `+${this.numberOverLimit}`;
+ },
+ shouldRenderCounter() {
+ if (this.issue.assignees.length <= this.maxRender) {
return false;
- },
- showLabelFooter() {
- return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
- },
- },
- methods: {
- isIndexLessThanlimit(index) {
- return index < this.limitBeforeCounter;
- },
- shouldRenderAssignee(index) {
- // Eg. maxRender is 4,
- // Render up to all 4 assignees if there are only 4 assigness
- // Otherwise render up to the limitBeforeCounter
- if (this.issue.assignees.length <= this.maxRender) {
- return index < this.maxRender;
- }
+ }
+
+ return this.issue.assignees.length > this.numberOverLimit;
+ },
+ issueId() {
+ if (this.issue.iid) {
+ return `#${this.issue.iid}`;
+ }
+ return false;
+ },
+ showLabelFooter() {
+ return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
+ },
+ issueReferencePath() {
+ const { referencePath, groupId } = this.issue;
+ return !groupId ? referencePath.split('#')[0] : null;
+ },
+ },
+ methods: {
+ isIndexLessThanlimit(index) {
+ return index < this.limitBeforeCounter;
+ },
+ shouldRenderAssignee(index) {
+ // Eg. maxRender is 4,
+ // Render up to all 4 assignees if there are only 4 assigness
+ // Otherwise render up to the limitBeforeCounter
+ if (this.issue.assignees.length <= this.maxRender) {
+ return index < this.maxRender;
+ }
+
+ return index < this.limitBeforeCounter;
+ },
+ assigneeUrl(assignee) {
+ if (!assignee) return '';
+ return `${this.rootPath}${assignee.username}`;
+ },
+ avatarUrlTitle(assignee) {
+ return `Avatar for ${assignee.name}`;
+ },
+ showLabel(label) {
+ if (!label.id) return false;
+ return true;
+ },
+ filterByLabel(label) {
+ if (!this.updateFilters) return;
+ const labelTitle = encodeURIComponent(label.title);
+ const filter = `label_name[]=${labelTitle}`;
+
+ this.applyFilter(filter);
+ },
+ filterByWeight(weight) {
+ if (!this.updateFilters) return;
- return index < this.limitBeforeCounter;
- },
- assigneeUrl(assignee) {
- return `${this.rootPath}${assignee.username}`;
- },
- assigneeUrlTitle(assignee) {
- return `Assigned to ${assignee.name}`;
- },
- avatarUrlTitle(assignee) {
- return `Avatar for ${assignee.name}`;
- },
- showLabel(label) {
- if (!label.id) return false;
- return true;
- },
- filterByLabel(label, e) {
- if (!this.updateFilters) return;
+ const issueWeight = encodeURIComponent(weight);
+ const filter = `weight=${issueWeight}`;
- const filterPath = boardsStore.filter.path.split('&');
- const labelTitle = encodeURIComponent(label.title);
- const param = `label_name[]=${labelTitle}`;
- const labelIndex = filterPath.indexOf(param);
- $(e.currentTarget).tooltip('hide');
+ this.applyFilter(filter);
+ },
+ applyFilter(filter) {
+ const filterPath = boardsStore.filter.path.split('&');
+ const filterIndex = filterPath.indexOf(filter);
- if (labelIndex === -1) {
- filterPath.push(param);
- } else {
- filterPath.splice(labelIndex, 1);
- }
+ if (filterIndex === -1) {
+ filterPath.push(filter);
+ } else {
+ filterPath.splice(filterIndex, 1);
+ }
- boardsStore.filter.path = filterPath.join('&');
+ boardsStore.filter.path = filterPath.join('&');
- boardsStore.updateFiltersUrl();
+ boardsStore.updateFiltersUrl();
- eventHub.$emit('updateTokens');
- },
- labelStyle(label) {
- return {
- backgroundColor: label.color,
- color: label.textColor,
- };
- },
- },
- };
+ eventHub.$emit('updateTokens');
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.textColor,
+ };
+ },
+ },
+};
</script>
<template>
<div>
<div class="board-card-header">
- <h4 class="board-card-title">
+ <h4 class="board-card-title append-bottom-0 prepend-top-0">
<icon
v-if="issue.confidential"
+ v-gl-tooltip
name="eye-slash"
- class="confidential-icon"
- />
- <a
+ :title="__('Confidential')"
+ class="confidential-icon append-right-4"
+ :aria-label="__('Confidential')"
+ /><a
:href="issue.path"
:title="issue.title"
class="js-no-trigger"
@mousemove.stop>{{ issue.title }}</a>
+ </h4>
+ </div>
+ <div
+ v-if="showLabelFooter"
+ class="board-card-labels prepend-top-4 d-flex flex-wrap"
+ >
+ <button
+ v-for="label in issue.labels"
+ v-if="showLabel(label)"
+ :key="label.id"
+ v-gl-tooltip
+ :style="labelStyle(label)"
+ :title="label.description"
+ class="badge color-label append-right-4 prepend-top-4"
+ type="button"
+ @click="filterByLabel(label)"
+ >
+ {{ label.title }}
+ </button>
+ </div>
+ <div class="board-card-footer d-flex justify-content-between align-items-end">
+ <div class="d-flex align-items-start flex-wrap-reverse board-card-number-container js-board-card-number-container">
<span
- v-if="issueId"
- class="board-card-number append-right-5"
+ v-if="issue.referencePath"
+ class="board-card-number d-flex append-right-8 prepend-top-8"
>
- {{ issue.referencePath }}
+ <tooltip-on-truncate
+ v-if="issueReferencePath"
+ :title="issueReferencePath"
+ placement="bottom"
+ class="board-issue-path block-truncated bold"
+ >{{ issueReferencePath }}</tooltip-on-truncate>#{{ issue.iid }}
</span>
- </h4>
+ <span class="board-info-items prepend-top-8 d-inline-block">
+ <issue-due-date
+ v-if="issue.dueDate"
+ :date="issue.dueDate"
+ /><issue-time-estimate
+ v-if="issue.timeEstimate"
+ :estimate="issue.timeEstimate"
+ />
+ </span>
+ </div>
<div class="board-card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
@@ -167,38 +225,26 @@
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatar"
- :tooltip-text="assigneeUrlTitle(assignee)"
+ :img-size="24"
class="js-no-trigger"
tooltip-placement="bottom"
- />
+ >
+ <span class="js-assignee-tooltip">
+ <span class="bold d-block">Assignee</span>
+ {{ assignee.name }}
+ <span class="text-white-50">@{{ assignee.username }}</span>
+ </span>
+ </user-avatar-link>
<span
v-if="shouldRenderCounter"
- v-tooltip
+ v-gl-tooltip
:title="assigneeCounterTooltip"
class="avatar-counter"
+ data-placement="bottom"
>
{{ assigneeCounterLabel }}
</span>
</div>
</div>
- <div
- v-if="showLabelFooter"
- class="board-card-footer"
- >
- <button
- v-for="label in issue.labels"
- v-if="showLabel(label)"
- :key="label.id"
- v-tooltip
- :style="labelStyle(label)"
- :title="label.description"
- class="badge color-label"
- type="button"
- data-container="body"
- @click="filterByLabel(label, $event)"
- >
- {{ label.title }}
- </button>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
new file mode 100644
index 00000000000..025ef7e9743
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -0,0 +1,90 @@
+<script>
+import dateFormat from 'dateformat';
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __ } from '~/locale';
+import { getDayDifference, getTimeago, dateInWords } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ Icon,
+ GlTooltip,
+ },
+ props: {
+ date: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ const timeago = getTimeago();
+ const { timeDifference, standardDateFormat } = this;
+ const formatedDate = standardDateFormat;
+
+ if (timeDifference >= -1 && timeDifference < 7) {
+ return `${timeago.format(this.issueDueDate)} (${formatedDate})`;
+ }
+
+ return timeago.format(this.issueDueDate);
+ },
+ body() {
+ const { timeDifference, issueDueDate, standardDateFormat } = this;
+
+ if (timeDifference === 0) {
+ return __('Today');
+ } else if (timeDifference === 1) {
+ return __('Tomorrow');
+ } else if (timeDifference === -1) {
+ return __('Yesterday');
+ } else if (timeDifference > 0 && timeDifference < 7) {
+ return dateFormat(issueDueDate, 'dddd', true);
+ }
+
+ return standardDateFormat;
+ },
+ issueDueDate() {
+ return new Date(this.date);
+ },
+ timeDifference() {
+ const today = new Date();
+ return getDayDifference(today, this.issueDueDate);
+ },
+ isPastDue() {
+ if (this.timeDifference >= 0) return false;
+ return true;
+ },
+ standardDateFormat() {
+ const today = new Date();
+ const isDueInCurrentYear = today.getFullYear() === this.issueDueDate.getFullYear();
+
+ return dateInWords(this.issueDueDate, true, isDueInCurrentYear);
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <span
+ ref="issueDueDate"
+ class="board-card-info card-number"
+ >
+ <icon
+ :class="{'text-danger': isPastDue, 'board-card-info-icon': true}"
+ name="calendar"
+ /><time
+ :class="{'text-danger': isPastDue}"
+ datetime="date"
+ class="board-card-info-text">{{ body }}</time>
+ </span>
+ <gl-tooltip
+ :target="() => $refs.issueDueDate"
+ placement="bottom"
+ >
+ <span class="bold">{{ __('Due date') }}</span>
+ <br />
+ <span :class="{'text-danger-muted': isPastDue}">{{ title }}</span>
+ </gl-tooltip>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
new file mode 100644
index 00000000000..efc7daf7812
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ Icon,
+ GlTooltip,
+ },
+ props: {
+ estimate: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return stringifyTime(parseSeconds(this.estimate), true);
+ },
+ timeEstimate() {
+ return stringifyTime(parseSeconds(this.estimate));
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <span
+ ref="issueTimeEstimate"
+ class="board-card-info card-number"
+ >
+ <icon
+ name="hourglass"
+ css-classes="board-card-info-icon"
+ /><time class="board-card-info-text">{{ timeEstimate }}</time>
+ </span>
+ <gl-tooltip
+ :target="() => $refs.issueTimeEstimate"
+ placement="bottom"
+ class="js-issue-time-estimate"
+ >
+ <span class="bold d-block">{{ __('Time estimate') }}</span>
+ {{ title }}
+ </gl-tooltip>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index dbd69f84526..795ba864545 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -20,7 +20,7 @@ export default {
computed: {
contents() {
const obj = {
- title: 'You haven\'t added any issues to your project yet',
+ title: "You haven't added any issues to your project yet",
content: `
An issue can be a bug, a todo or a feature request that needs to be
discussed in a project. Besides, issues are searchable and filterable.
@@ -28,7 +28,7 @@ export default {
};
if (this.activeTab === 'selected') {
- obj.title = 'You haven\'t selected any issues yet';
+ obj.title = "You haven't selected any issues yet";
obj.content = `
Go back to <strong>Open issues</strong> and select some issues
to add to your board.
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index 268ca6bca13..d51597ed22d 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -42,19 +42,17 @@ export default {
const req = this.buildUpdateRequest(list);
// Post the data to the backend
- gl.boardService
- .bulkUpdate(issueIds, req)
- .catch(() => {
- Flash(__('Failed to update issues, please try again.'));
+ gl.boardService.bulkUpdate(issueIds, req).catch(() => {
+ Flash(__('Failed to update issues, please try again.'));
- selectedIssues.forEach((issue) => {
- list.removeIssue(issue);
- list.issuesSize -= 1;
- });
+ selectedIssues.forEach(issue => {
+ list.removeIssue(issue);
+ list.issuesSize -= 1;
});
+ });
// Add the issues on the frontend
- selectedIssues.forEach((issue) => {
+ selectedIssues.forEach(issue => {
list.addIssue(issue);
list.issuesSize += 1;
});
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 979fb4d7199..fc6cefa89a9 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -1,52 +1,52 @@
<script>
- import ModalFilters from './filters';
- import ModalTabs from './tabs.vue';
- import ModalStore from '../../stores/modal_store';
- import modalMixin from '../../mixins/modal_mixins';
+import ModalFilters from './filters';
+import ModalTabs from './tabs.vue';
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
- export default {
- components: {
- ModalTabs,
- ModalFilters,
+export default {
+ components: {
+ ModalTabs,
+ ModalFilters,
+ },
+ mixins: [modalMixin],
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
},
- mixins: [modalMixin],
- props: {
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
+ milestonePath: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ labelPath: {
+ type: String,
+ required: true,
},
- computed: {
- selectAllText() {
- if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
- return 'Select all';
- }
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectAllText() {
+ if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+ return 'Select all';
+ }
- return 'Deselect all';
- },
- showSearch() {
- return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
- },
+ return 'Deselect all';
},
- methods: {
- toggleAll() {
- this.$refs.selectAllBtn.blur();
+ showSearch() {
+ return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
+ },
+ },
+ methods: {
+ toggleAll() {
+ this.$refs.selectAllBtn.blur();
- ModalStore.toggleAll();
- },
+ ModalStore.toggleAll();
},
- };
+ },
+};
</script>
<template>
<div>
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 0c4c709324d..fdd1346d4c7 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -1,143 +1,146 @@
<script>
- /* global ListIssue */
- import { urlParamsToObject } from '~/lib/utils/common_utils';
- import ModalHeader from './header.vue';
- import ModalList from './list.vue';
- import ModalFooter from './footer.vue';
- import EmptyState from './empty_state.vue';
- import ModalStore from '../../stores/modal_store';
+/* global ListIssue */
+import { urlParamsToObject } from '~/lib/utils/common_utils';
+import ModalHeader from './header.vue';
+import ModalList from './list.vue';
+import ModalFooter from './footer.vue';
+import EmptyState from './empty_state.vue';
+import ModalStore from '../../stores/modal_store';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
- export default {
- components: {
- EmptyState,
- ModalHeader,
- ModalList,
- ModalFooter,
+export default {
+ components: {
+ EmptyState,
+ ModalHeader,
+ ModalList,
+ ModalFooter,
+ GlLoadingIcon,
+ },
+ props: {
+ newIssuePath: {
+ type: String,
+ required: true,
},
- props: {
- newIssuePath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
+ emptyStateSvg: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ issueLinkBase: {
+ type: String,
+ required: true,
},
- computed: {
- showList() {
- if (this.activeTab === 'selected') {
- return this.selectedIssues.length > 0;
- }
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ showList() {
+ if (this.activeTab === 'selected') {
+ return this.selectedIssues.length > 0;
+ }
- return this.issuesCount > 0;
- },
- showEmptyState() {
- if (!this.loading && this.issuesCount === 0) {
- return true;
- }
+ return this.issuesCount > 0;
+ },
+ showEmptyState() {
+ if (!this.loading && this.issuesCount === 0) {
+ return true;
+ }
- return this.activeTab === 'selected' && this.selectedIssues.length === 0;
- },
+ return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
- watch: {
- page() {
- this.loadIssues();
- },
- showAddIssuesModal() {
- if (this.showAddIssuesModal && !this.issues.length) {
- this.loading = true;
+ },
+ watch: {
+ page() {
+ this.loadIssues();
+ },
+ showAddIssuesModal() {
+ if (this.showAddIssuesModal && !this.issues.length) {
+ this.loading = true;
+ const loadingDone = () => {
+ this.loading = false;
+ };
+
+ this.loadIssues()
+ .then(loadingDone)
+ .catch(loadingDone);
+ } else if (!this.showAddIssuesModal) {
+ this.issues = [];
+ this.selectedIssues = [];
+ this.issuesCount = false;
+ }
+ },
+ filter: {
+ handler() {
+ if (this.$el.tagName) {
+ this.page = 1;
+ this.filterLoading = true;
const loadingDone = () => {
- this.loading = false;
+ this.filterLoading = false;
};
- this.loadIssues()
+ this.loadIssues(true)
.then(loadingDone)
.catch(loadingDone);
- } else if (!this.showAddIssuesModal) {
- this.issues = [];
- this.selectedIssues = [];
- this.issuesCount = false;
}
},
- filter: {
- handler() {
- if (this.$el.tagName) {
- this.page = 1;
- this.filterLoading = true;
- const loadingDone = () => {
- this.filterLoading = false;
- };
-
- this.loadIssues(true)
- .then(loadingDone)
- .catch(loadingDone);
- }
- },
- deep: true,
- },
+ deep: true,
},
- created() {
- this.page = 1;
- },
- methods: {
- loadIssues(clearIssues = false) {
- if (!this.showAddIssuesModal) return false;
+ },
+ created() {
+ this.page = 1;
+ },
+ methods: {
+ loadIssues(clearIssues = false) {
+ if (!this.showAddIssuesModal) return false;
- return gl.boardService.getBacklog({
+ return gl.boardService
+ .getBacklog({
...urlParamsToObject(this.filter.path),
page: this.page,
per: this.perPage,
})
- .then(res => res.data)
- .then(data => {
- if (clearIssues) {
- this.issues = [];
- }
+ .then(res => res.data)
+ .then(data => {
+ if (clearIssues) {
+ this.issues = [];
+ }
- data.issues.forEach(issueObj => {
- const issue = new ListIssue(issueObj);
- const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = !!foundSelectedIssue;
+ data.issues.forEach(issueObj => {
+ const issue = new ListIssue(issueObj);
+ const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
+ issue.selected = !!foundSelectedIssue;
- this.issues.push(issue);
- });
+ this.issues.push(issue);
+ });
- this.loadingNewPage = false;
+ this.loadingNewPage = false;
- if (!this.issuesCount) {
- this.issuesCount = data.size;
- }
- })
- .catch(() => {
- // TODO: handle request error
- });
- },
+ if (!this.issuesCount) {
+ this.issuesCount = data.size;
+ }
+ })
+ .catch(() => {
+ // TODO: handle request error
+ });
},
- };
+ },
+};
</script>
<template>
<div
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index c93fd9f415c..e11f398e70d 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -1,120 +1,120 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
- import bp from '../../../breakpoints';
- import ModalStore from '../../stores/modal_store';
- import IssueCardInner from '../issue_card_inner.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import bp from '../../../breakpoints';
+import ModalStore from '../../stores/modal_store';
+import IssueCardInner from '../issue_card_inner.vue';
- export default {
- components: {
- IssueCardInner,
- Icon,
+export default {
+ components: {
+ IssueCardInner,
+ Icon,
+ },
+ props: {
+ issueLinkBase: {
+ type: String,
+ required: true,
},
- props: {
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
+ rootPath: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ emptyStateSvg: {
+ type: String,
+ required: true,
},
- computed: {
- loopIssues() {
- if (this.activeTab === 'all') {
- return this.issues;
- }
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ loopIssues() {
+ if (this.activeTab === 'all') {
+ return this.issues;
+ }
- return this.selectedIssues;
- },
- groupedIssues() {
- const groups = [];
- this.loopIssues.forEach((issue, i) => {
- const index = i % this.columns;
+ return this.selectedIssues;
+ },
+ groupedIssues() {
+ const groups = [];
+ this.loopIssues.forEach((issue, i) => {
+ const index = i % this.columns;
- if (!groups[index]) {
- groups.push([]);
- }
+ if (!groups[index]) {
+ groups.push([]);
+ }
- groups[index].push(issue);
- });
+ groups[index].push(issue);
+ });
- return groups;
- },
+ return groups;
},
- watch: {
- activeTab() {
- if (this.activeTab === 'all') {
- ModalStore.purgeUnselectedIssues();
- }
- },
+ },
+ watch: {
+ activeTab() {
+ if (this.activeTab === 'all') {
+ ModalStore.purgeUnselectedIssues();
+ }
},
- mounted() {
- this.scrollHandlerWrapper = this.scrollHandler.bind(this);
- this.setColumnCountWrapper = this.setColumnCount.bind(this);
- this.setColumnCount();
+ },
+ mounted() {
+ this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+ this.setColumnCountWrapper = this.setColumnCount.bind(this);
+ this.setColumnCount();
- this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
- window.addEventListener('resize', this.setColumnCountWrapper);
+ this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
+ window.addEventListener('resize', this.setColumnCountWrapper);
+ },
+ beforeDestroy() {
+ this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
+ window.removeEventListener('resize', this.setColumnCountWrapper);
+ },
+ methods: {
+ scrollHandler() {
+ const currentPage = Math.floor(this.issues.length / this.perPage);
+
+ if (
+ this.scrollTop() > this.scrollHeight() - 100 &&
+ !this.loadingNewPage &&
+ currentPage === this.page
+ ) {
+ this.loadingNewPage = true;
+ this.page += 1;
+ }
},
- beforeDestroy() {
- this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
- window.removeEventListener('resize', this.setColumnCountWrapper);
+ toggleIssue(e, issue) {
+ if (e.target.tagName !== 'A') {
+ ModalStore.toggleIssue(issue);
+ }
},
- methods: {
- scrollHandler() {
- const currentPage = Math.floor(this.issues.length / this.perPage);
-
- if (
- this.scrollTop() > this.scrollHeight() - 100 &&
- !this.loadingNewPage &&
- currentPage === this.page
- ) {
- this.loadingNewPage = true;
- this.page += 1;
- }
- },
- toggleIssue(e, issue) {
- if (e.target.tagName !== 'A') {
- ModalStore.toggleIssue(issue);
- }
- },
- listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
- },
- scrollHeight() {
- return this.$refs.list.scrollHeight;
- },
- scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
- },
- showIssue(issue) {
- if (this.activeTab === 'all') return true;
+ listHeight() {
+ return this.$refs.list.getBoundingClientRect().height;
+ },
+ scrollHeight() {
+ return this.$refs.list.scrollHeight;
+ },
+ scrollTop() {
+ return this.$refs.list.scrollTop + this.listHeight();
+ },
+ showIssue(issue) {
+ if (this.activeTab === 'all') return true;
- const index = ModalStore.selectedIssueIndex(issue);
+ const index = ModalStore.selectedIssueIndex(issue);
- return index !== -1;
- },
- setColumnCount() {
- const breakpoint = bp.getBreakpointSize();
+ return index !== -1;
+ },
+ setColumnCount() {
+ const breakpoint = bp.getBreakpointSize();
- if (breakpoint === 'lg' || breakpoint === 'md') {
- this.columns = 3;
- } else if (breakpoint === 'sm') {
- this.columns = 2;
- } else {
- this.columns = 1;
- }
- },
+ if (breakpoint === 'lg' || breakpoint === 'md') {
+ this.columns = 3;
+ } else if (breakpoint === 'sm') {
+ this.columns = 2;
+ } else {
+ this.columns = 1;
+ }
},
- };
+ },
+};
</script>
<template>
<section
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
index d926b080094..5d661590e8e 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.vue
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -1,21 +1,21 @@
<script>
- import ModalStore from '../../stores/modal_store';
- import modalMixin from '../../mixins/modal_mixins';
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
- export default {
- mixins: [modalMixin],
- data() {
- return ModalStore.store;
+export default {
+ mixins: [modalMixin],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
},
- computed: {
- selectedCount() {
- return ModalStore.selectedCount();
- },
- },
- destroyed() {
- this.activeTab = 'all';
- },
- };
+ },
+ destroyed() {
+ this.activeTab = 'all';
+ },
+};
</script>
<template>
<div class="top-area prepend-top-10 append-bottom-10">
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 2c2045f8901..f7016561f93 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -6,36 +6,41 @@ import _ from 'underscore';
import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store';
-$(document).off('created.label').on('created.label', (e, label) => {
- boardsStore.new({
- title: label.title,
- position: boardsStore.state.lists.length - 2,
- list_type: 'label',
- label: {
- id: label.id,
+$(document)
+ .off('created.label')
+ .on('created.label', (e, label) => {
+ boardsStore.new({
title: label.title,
- color: label.color,
- },
+ position: boardsStore.state.lists.length - 2,
+ list_type: 'label',
+ label: {
+ id: label.id,
+ title: label.title,
+ color: label.color,
+ },
+ });
});
-});
export default function initNewListDropdown() {
- $('.js-new-board-list').each(function () {
+ $('.js-new-board-list').each(function() {
const $this = $(this);
- new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath'));
+ new CreateLabelDropdown(
+ $this.closest('.dropdown').find('.dropdown-new-label'),
+ $this.data('namespacePath'),
+ $this.data('projectPath'),
+ );
$this.glDropdown({
data(term, callback) {
- axios.get($this.attr('data-list-labels-path'))
- .then(({ data }) => {
- callback(data);
- });
+ axios.get($this.attr('data-list-labels-path')).then(({ data }) => {
+ callback(data);
+ });
},
- renderRow (label) {
+ renderRow(label) {
const active = boardsStore.findList('title', label.title);
const $li = $('<li />');
const $a = $('<a />', {
- class: (active ? `is-active js-board-list-${active.id}` : ''),
+ class: active ? `is-active js-board-list-${active.id}` : '',
text: label.title,
href: '#',
});
@@ -53,7 +58,7 @@ export default function initNewListDropdown() {
selectable: true,
multiSelect: true,
containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
- clicked (options) {
+ clicked(options) {
const { e } = options;
const label = options.selectedObj;
e.preventDefault();
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 427a0868b0c..503417644fa 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -2,6 +2,7 @@
import $ from 'jquery';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
import Api from '../../api';
@@ -9,6 +10,7 @@ export default {
name: 'BoardProjectSelect',
components: {
Icon,
+ GlLoadingIcon,
},
props: {
groupId: {
@@ -46,7 +48,7 @@ export default {
selectable: true,
data: (term, callback) => {
this.loading = true;
- return Api.groupProjects(this.groupId, term, {with_issues_enabled: true}, projects => {
+ return Api.groupProjects(this.groupId, term, { with_issues_enabled: true }, projects => {
this.loading = false;
callback(projects);
});
@@ -54,7 +56,9 @@ export default {
renderRow(project) {
return `
<li>
- <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
+ <a href='#' class='dropdown-menu-link' data-project-id="${
+ project.id
+ }" data-project-name="${project.name}">
${_.escape(project.name)}
</a>
</li>
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
index b8f2e324d43..d681e6a431c 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -1,79 +1,77 @@
<script>
- import Vue from 'vue';
- import Flash from '../../../flash';
- import { __ } from '../../../locale';
- import boardsStore from '../../stores/boards_store';
+import Vue from 'vue';
+import Flash from '../../../flash';
+import { __ } from '../../../locale';
+import boardsStore from '../../stores/boards_store';
- export default Vue.extend({
- props: {
- issue: {
- type: Object,
- required: true,
- },
- list: {
- type: Object,
- required: true,
- },
+export default Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
},
- computed: {
- updateUrl() {
- return this.issue.path;
- },
+ list: {
+ type: Object,
+ required: true,
},
- methods: {
- removeIssue() {
- const { issue } = this;
- const lists = issue.getLists();
- const req = this.buildPatchRequest(issue, lists);
-
- const data = {
- issue: this.seedPatchRequest(issue, req),
- };
+ },
+ computed: {
+ updateUrl() {
+ return this.issue.path;
+ },
+ },
+ methods: {
+ removeIssue() {
+ const { issue } = this;
+ const lists = issue.getLists();
+ const req = this.buildPatchRequest(issue, lists);
- if (data.issue.label_ids.length === 0) {
- data.issue.label_ids = [''];
- }
+ const data = {
+ issue: this.seedPatchRequest(issue, req),
+ };
- // Post the remove data
- Vue.http.patch(this.updateUrl, data).catch(() => {
- Flash(__('Failed to remove issue from board, please try again.'));
+ if (data.issue.label_ids.length === 0) {
+ data.issue.label_ids = [''];
+ }
- lists.forEach(list => {
- list.addIssue(issue);
- });
- });
+ // Post the remove data
+ Vue.http.patch(this.updateUrl, data).catch(() => {
+ Flash(__('Failed to remove issue from board, please try again.'));
- // Remove from the frontend store
lists.forEach(list => {
- list.removeIssue(issue);
+ list.addIssue(issue);
});
+ });
- boardsStore.detail.issue = {};
- },
- /**
- * Build the default patch request.
- */
- buildPatchRequest(issue, lists) {
- const listLabelIds = lists.map(list => list.label.id);
+ // Remove from the frontend store
+ lists.forEach(list => {
+ list.removeIssue(issue);
+ });
- const labelIds = issue.labels
- .map(label => label.id)
- .filter(id => !listLabelIds.includes(id));
+ boardsStore.detail.issue = {};
+ },
+ /**
+ * Build the default patch request.
+ */
+ buildPatchRequest(issue, lists) {
+ const listLabelIds = lists.map(list => list.label.id);
+
+ const labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
- return {
- label_ids: labelIds,
- };
- },
- /**
- * Seed the given patch request.
- *
- * (This is overridden in EE)
- */
- seedPatchRequest(issue, req) {
- return req;
- },
+ return {
+ label_ids: labelIds,
+ };
+ },
+ /**
+ * Seed the given patch request.
+ *
+ * (This is overridden in EE)
+ */
+ seedPatchRequest(issue, req) {
+ return req;
},
- });
+ },
+});
</script>
<template>
<div
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index acf41e5689e..c14d69c5d18 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -32,7 +32,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
// Remove all the tokens as they will be replaced by the search manager
- [].forEach.call(tokens, (el) => {
+ [].forEach.call(tokens, el => {
el.parentNode.removeChild(el);
});
@@ -50,7 +50,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
canEdit(tokenName, tokenValue) {
if (this.cantEdit.includes(tokenName)) return false;
- return this.cantEditWithValue.findIndex(token => token.name === tokenName &&
- token.value === tokenValue) === -1;
+ return (
+ this.cantEditWithValue.findIndex(
+ token => token.name === tokenName && token.value === tokenValue,
+ ) === -1
+ );
}
}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 91861f2f9ee..61a3072ac27 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -32,9 +32,9 @@ export default () => {
const $boardApp = document.getElementById('board-app');
// check for browser back and trigger a hard reload to circumvent browser caching.
- window.addEventListener('pageshow', (event) => {
- const isNavTypeBackForward = window.performance &&
- window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
+ window.addEventListener('pageshow', event => {
+ const isNavTypeBackForward =
+ window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
if (event.persisted || isNavTypeBackForward) {
window.location.reload();
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index c9cde4effb9..983b28d2e67 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -4,7 +4,8 @@ import $ from 'jquery';
import sortableConfig from '../../sortable/sortable_config';
export function sortableStart() {
- $('.has-tooltip').tooltip('hide')
+ $('.has-tooltip')
+ .tooltip('hide')
.tooltip('disable');
document.body.classList.add('is-dragging');
}
@@ -15,7 +16,8 @@ export function sortableEnd() {
}
export function getBoardSortableDefaultOptions(obj) {
- const touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
+ const touchEnabled =
+ 'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn',
@@ -26,6 +28,8 @@ export function getBoardSortableDefaultOptions(obj) {
onEnd: sortableEnd,
});
- Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
+ Object.keys(obj).forEach(key => {
+ defaultSortOptions[key] = obj[key];
+ });
return defaultSortOptions;
}
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 52d04389b88..5e0f0b07247 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -9,7 +9,7 @@ import IssueProject from './project';
import boardsStore from '../stores/boards_store';
class ListIssue {
- constructor (obj, defaultAvatar) {
+ constructor(obj, defaultAvatar) {
this.id = obj.id;
this.iid = obj.iid;
this.title = obj.title;
@@ -30,6 +30,8 @@ class ListIssue {
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id;
+ this.timeEstimate = obj.time_estimate;
+ this.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
if (obj.project) {
this.project = new IssueProject(obj.project);
@@ -39,54 +41,54 @@ class ListIssue {
this.milestone = new ListMilestone(obj.milestone);
}
- obj.labels.forEach((label) => {
+ obj.labels.forEach(label => {
this.labels.push(new ListLabel(label));
});
this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
}
- addLabel (label) {
+ addLabel(label) {
if (!this.findLabel(label)) {
this.labels.push(new ListLabel(label));
}
}
- findLabel (findLabel) {
+ findLabel(findLabel) {
return this.labels.filter(label => label.title === findLabel.title)[0];
}
- removeLabel (removeLabel) {
+ removeLabel(removeLabel) {
if (removeLabel) {
this.labels = this.labels.filter(label => removeLabel.title !== label.title);
}
}
- removeLabels (labels) {
+ removeLabels(labels) {
labels.forEach(this.removeLabel.bind(this));
}
- addAssignee (assignee) {
+ addAssignee(assignee) {
if (!this.findAssignee(assignee)) {
this.assignees.push(new ListAssignee(assignee));
}
}
- findAssignee (findAssignee) {
+ findAssignee(findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
}
- removeAssignee (removeAssignee) {
+ removeAssignee(removeAssignee) {
if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
}
}
- removeAllAssignees () {
+ removeAllAssignees() {
this.assignees = [];
}
- getLists () {
+ getLists() {
return boardsStore.state.lists.filter(list => list.findIssue(this.id));
}
@@ -102,14 +104,14 @@ class ListIssue {
this.isLoading[key] = value;
}
- update () {
+ update() {
const data = {
issue: {
milestone_id: this.milestone ? this.milestone.id : null,
due_date: this.dueDate,
- assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0],
- label_ids: this.labels.map((label) => label.id)
- }
+ assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0],
+ label_ids: this.labels.map(label => label.id),
+ },
};
if (!data.issue.label_ids.length) {
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 3161f1da8c9..dd3feedbc0e 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -234,11 +234,11 @@ class List {
});
}
- getTypeInfo (type) {
+ getTypeInfo(type) {
return TYPES[type] || {};
}
- onNewIssueResponse (issue, data) {
+ onNewIssueResponse(issue, data) {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 029b0971f2c..3de6eb056c2 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -19,7 +19,9 @@ export default class BoardService {
}
static generateIssuePath(boardId, id) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
+ return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
+ id ? `/${id}` : ''
+ }`;
}
all() {
@@ -54,7 +56,9 @@ export default class BoardService {
getIssuesForList(id, filter = {}) {
const data = { id };
- Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
+ Object.keys(filter).forEach(key => {
+ data[key] = filter[key];
+ });
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
}
@@ -75,7 +79,9 @@ export default class BoardService {
}
getBacklog(data) {
- return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`));
+ return axios.get(
+ mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`),
+ );
}
bulkUpdate(issueIds, extraData = {}) {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 471955747fd..eefe14a1d79 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -20,20 +20,20 @@ const boardsStore = {
issue: {},
list: {},
},
- create () {
+ create() {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
this.detail = {
issue: {},
};
},
- addList (listObj, defaultAvatar) {
+ addList(listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
this.state.lists.push(list);
return list;
},
- new (listObj) {
+ new(listObj) {
const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
@@ -50,44 +50,44 @@ const boardsStore = {
});
this.removeBlankState();
},
- updateNewListDropdown (listId) {
+ updateNewListDropdown(listId) {
$(`.js-board-list-${listId}`).removeClass('is-active');
},
- shouldAddBlankState () {
+ shouldAddBlankState() {
// Decide whether to add the blank state
- return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]);
+ return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0];
},
- addBlankState () {
+ addBlankState() {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
this.addList({
id: 'blank',
list_type: 'blank',
title: 'Welcome to your Issue Board!',
- position: 0
+ position: 0,
});
this.state.lists = _.sortBy(this.state.lists, 'position');
},
- removeBlankState () {
+ removeBlankState() {
this.removeList('blank');
Cookies.set('issue_board_welcome_hidden', 'true', {
expires: 365 * 10,
- path: ''
+ path: '',
});
},
- welcomeIsHidden () {
+ welcomeIsHidden() {
return Cookies.get('issue_board_welcome_hidden') === 'true';
},
- removeList (id, type = 'blank') {
+ removeList(id, type = 'blank') {
const list = this.findList('id', id, type);
if (!list) return;
this.state.lists = this.state.lists.filter(list => list.id !== id);
},
- moveList (listFrom, orderLists) {
+ moveList(listFrom, orderLists) {
orderLists.forEach((id, i) => {
const list = this.findList('id', parseInt(id, 10));
@@ -95,22 +95,25 @@ const boardsStore = {
});
listFrom.update();
},
- moveIssueToList (listFrom, listTo, issue, newIndex) {
+ moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) {
// Check if target list assignee is already present in this issue
- if ((listTo.type === 'assignee' && listFrom.type === 'assignee') &&
- issue.findAssignee(listTo.assignee)) {
+ if (
+ listTo.type === 'assignee' &&
+ listFrom.type === 'assignee' &&
+ issue.findAssignee(listTo.assignee)
+ ) {
const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee);
} else if (listTo.type === 'milestone') {
const currentMilestone = issue.milestone;
const currentLists = this.state.lists
- .filter(list => (list.type === 'milestone' && list.id !== listTo.id))
- .filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
+ .filter(list => list.type === 'milestone' && list.id !== listTo.id)
+ .filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
issue.removeMilestone(currentMilestone);
issue.addMilestone(listTo.milestone);
@@ -126,7 +129,7 @@ const boardsStore = {
}
if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
- issueLists.forEach((list) => {
+ issueLists.forEach(list => {
list.removeIssue(issue);
});
issue.removeLabels(listLabels);
@@ -144,26 +147,28 @@ const boardsStore = {
return (
(listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') ||
- (listFrom.type === 'backlog')
+ listFrom.type === 'backlog'
);
},
- moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
+ moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
- findList (key, val, type = 'label') {
- const filteredList = this.state.lists.filter((list) => {
- const byType = type ? (list.type === type) || (list.type === 'assignee') || (list.type === 'milestone') : true;
+ findList(key, val, type = 'label') {
+ const filteredList = this.state.lists.filter(list => {
+ const byType = type
+ ? list.type === type || list.type === 'assignee' || list.type === 'milestone'
+ : true;
return list[key] === val && byType;
});
return filteredList[0];
},
- updateFiltersUrl () {
+ updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`);
- }
+ },
};
// hacks added in order to allow milestone_select to function properly
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index 0d9ac367a70..b7228bf7bf5 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -40,7 +40,7 @@ class ModalStore {
toggleAll() {
const select = this.selectedCount() !== this.store.issues.length;
- this.store.issues.forEach((issue) => {
+ this.store.issues.forEach(issue => {
const issueUpdate = issue;
if (issueUpdate.selected !== select) {
@@ -69,13 +69,14 @@ class ModalStore {
removeSelectedIssue(issue, forcePurge = false) {
if (this.store.activeTab === 'all' || forcePurge) {
- this.store.selectedIssues = this.store.selectedIssues
- .filter(fIssue => fIssue.id !== issue.id);
+ this.store.selectedIssues = this.store.selectedIssues.filter(
+ fIssue => fIssue.id !== issue.id,
+ );
}
}
purgeUnselectedIssues() {
- this.store.selectedIssues.forEach((issue) => {
+ this.store.selectedIssues.forEach(issue => {
if (!issue.selected) {
this.removeSelectedIssue(issue, true);
}
@@ -87,8 +88,7 @@ class ModalStore {
}
findSelectedIssue(issue) {
- return this.store.selectedIssues
- .filter(filteredIssue => filteredIssue.id === issue.id)[0];
+ return this.store.selectedIssues.filter(filteredIssue => filteredIssue.id === issue.id)[0];
}
}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index b330b3fac89..cf70a48f076 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -9,7 +9,7 @@ import eventHub from './event_hub';
import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
-import applications from './components/applications.vue';
+import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
/**
@@ -29,8 +29,10 @@ export default class Clusters {
installCertManagerPath,
installRunnerPath,
installJupyterPath,
+ installKnativePath,
installPrometheusPath,
managePrometheusPath,
+ clusterType,
clusterStatus,
clusterStatusReason,
helpPath,
@@ -51,6 +53,7 @@ export default class Clusters {
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath,
+ installKnativeEndpoint: installKnativePath,
});
this.installApplication = this.installApplication.bind(this);
@@ -67,7 +70,7 @@ export default class Clusters {
initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
- this.initApplications();
+ this.initApplications(clusterType);
if (this.store.state.status !== 'created') {
this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
@@ -79,23 +82,21 @@ export default class Clusters {
}
}
- initApplications() {
+ initApplications(type) {
const { store } = this;
const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({
el,
- components: {
- applications,
- },
data() {
return {
state: store.state,
};
},
render(createElement) {
- return createElement('applications', {
+ return createElement(Applications, {
props: {
+ type,
applications: this.state.applications,
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
deleted file mode 100644
index 789c8360124..00000000000
--- a/app/assets/javascripts/clusters/clusters_index.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-import setupToggleButtons from '~/toggle_buttons';
-import initDismissableCallout from '~/dismissable_callout';
-
-import ClustersService from './services/clusters_service';
-
-export default () => {
- const clusterList = document.querySelector('.js-clusters-list');
-
- initDismissableCallout('.gcp-signup-offer');
-
- // 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 => {
- createFlash(__('Something went wrong on our end.'));
- throw err;
- },
- ),
- );
- }
-};
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 4f84d916e85..2229e659e1f 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,6 +1,6 @@
<script>
import _ from 'underscore';
-import helmInstallIllustration from '@gitlab-org/gitlab-svgs/dist/illustrations/kubernetes-installation.svg';
+import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png';
@@ -8,12 +8,13 @@ import jeagerLogo from 'images/cluster_app_logos/jeager.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import certManagerLogo from 'images/cluster_app_logos/cert_manager.png';
+import knativeLogo from 'images/cluster_app_logos/knative.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import { APPLICATION_STATUS, INGRESS } from '../constants';
+import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
export default {
components: {
@@ -21,6 +22,11 @@ export default {
clipboardButton,
},
props: {
+ type: {
+ type: String,
+ required: false,
+ default: CLUSTER_TYPE.PROJECT,
+ },
applications: {
type: Object,
required: false,
@@ -55,10 +61,14 @@ export default {
jupyterhubLogo,
kubernetesLogo,
certManagerLogo,
+ knativeLogo,
meltanoLogo,
prometheusLogo,
}),
computed: {
+ isProjectCluster() {
+ return this.type === CLUSTER_TYPE.PROJECT;
+ },
helmInstalled() {
return (
this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
@@ -141,6 +151,9 @@ export default {
jupyterHostname() {
return this.applications.jupyter.hostname;
},
+ knativeInstalled() {
+ return this.applications.knative.status === APPLICATION_STATUS.INSTALLED;
+ },
},
created() {
this.helmInstallIllustration = helmInstallIllustration;
@@ -321,6 +334,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="runner"
:logo-url="gitlabLogo"
:title="applications.runner.title"
@@ -339,6 +353,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="jupyter"
:logo-url="jupyterhubLogo"
:title="applications.jupyter.title"
@@ -348,7 +363,6 @@ export default {
:request-reason="applications.jupyter.requestReason"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
- class="hide-bottom-border rounded-bottom"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
>
<div slot="description">
@@ -398,6 +412,58 @@ export default {
</template>
</div>
</application-row>
+ <application-row
+ id="knative"
+ :logo-url="knativeLogo"
+ :title="applications.knative.title"
+ :status="applications.knative.status"
+ :status-reason="applications.knative.statusReason"
+ :request-status="applications.knative.requestStatus"
+ :request-reason="applications.knative.requestReason"
+ :install-application-request-params="{ hostname: applications.knative.hostname}"
+ :disabled="!helmInstalled"
+ class="hide-bottom-border rounded-bottom"
+ title-link="https://github.com/knative/docs"
+ >
+ <div slot="description">
+ <p>
+ {{ s__(`ClusterIntegration|A Knative build extends Kubernetes
+ and utilizes existing Kubernetes primitives to provide you with
+ the ability to run on-cluster container builds from source.
+ For example, you can write a build that uses Kubernetes-native
+ resources to obtain your source code from a repository,
+ build it into container a image, and then run that image.`) }}
+ </p>
+
+ <template v-if="knativeInstalled">
+ <div class="form-group">
+ <label for="knative-domainname">
+ {{ s__('ClusterIntegration|Knative Domain Name:') }}
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="applications.knative.hostname"
+ type="text"
+ class="form-control js-domainname"
+ readonly
+ />
+ </div>
+ </template>
+ <template v-else>
+ <div class="form-group">
+ <label for="knative-domainname">
+ {{ s__('ClusterIntegration|Knative Domain Name:') }}
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="applications.knative.hostname"
+ type="text"
+ class="form-control js-domainname"
+ />
+ </div>
+ </template>
+ </div>
+ </application-row>
</div>
</section>
</template>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 24a49624583..15cf4a56138 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -1,3 +1,10 @@
+// These need to match the enum found in app/models/clusters/cluster.rb
+export const CLUSTER_TYPE = {
+ INSTANCE: 'instance_type',
+ GROUP: 'group_type',
+ PROJECT: 'project_type',
+};
+
// These need to match what is returned from the server
export const APPLICATION_STATUS = {
NOT_INSTALLABLE: 'not_installable',
@@ -16,3 +23,4 @@ export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
+export const KNATIVE = 'knative';
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index 1abe244b0af..89dda4b7902 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -10,6 +10,7 @@ export default class ClusterService {
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint,
+ knative: this.options.installKnativeEndpoint,
};
}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 24ac35852fe..07f85880d06 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,5 +1,5 @@
import { s__ } from '../../locale';
-import { INGRESS, JUPYTER } from '../constants';
+import { INGRESS, JUPYTER, KNATIVE } from '../constants';
export default class ClusterStore {
constructor() {
@@ -54,6 +54,14 @@ export default class ClusterStore {
requestReason: null,
hostname: null,
},
+ knative: {
+ title: s__('ClusterIntegration|Knative'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ hostname: null,
+ },
},
};
}
@@ -101,6 +109,9 @@ export default class ClusterStore {
(this.state.applications.ingress.externalIp
? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
: '');
+ } else if (appId === KNATIVE) {
+ this.state.applications.knative.hostname =
+ serverAppEntry.hostname || this.state.applications.knative.hostname;
}
});
}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index a2aa3d197e3..82532539c9c 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -2,9 +2,15 @@
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines';
+import TablePagination from '../../vue_shared/components/table_pagination.vue';
+import { getParameterByName } from '../../lib/utils/common_utils';
+import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
- mixins: [pipelinesMixin],
+ components: {
+ TablePagination,
+ },
+ mixins: [pipelinesMixin, CIPaginationMixin],
props: {
endpoint: {
type: String,
@@ -35,6 +41,8 @@ export default {
return {
store,
state: store.state,
+ page: getParameterByName('page') || '1',
+ requestData: {},
};
},
@@ -48,11 +56,14 @@ export default {
},
created() {
this.service = new PipelinesService(this.endpoint);
+ this.requestData = { page: this.page };
},
methods: {
successCallback(resp) {
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = resp.data.pipelines || resp.data;
+
+ this.store.storePagination(resp.headers);
this.setCommonData(pipelines);
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
@@ -97,5 +108,11 @@ export default {
:view-type="viewType"
/>
</div>
+
+ <table-pagination
+ v-if="shouldRenderPagination"
+ :change="onChangePage"
+ :page-info="state.pageInfo"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
deleted file mode 100644
index f60665577fe..00000000000
--- a/app/assets/javascripts/commons/gitlab_ui.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Vue from 'vue';
-import {
- GlPagination,
- GlProgressBar,
- GlModal,
- GlLoadingIcon,
- GlModalDirective,
- GlTooltipDirective,
-} from '@gitlab-org/gitlab-ui';
-
-Vue.component('gl-pagination', GlPagination);
-Vue.component('gl-progress-bar', GlProgressBar);
-Vue.component('gl-ui-modal', GlModal);
-Vue.component('gl-loading-icon', GlLoadingIcon);
-
-Vue.directive('gl-modal', GlModalDirective);
-Vue.directive('gl-tooltip', GlTooltipDirective);
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index ea945cd3fa5..0d2fe2925d8 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -3,5 +3,4 @@ import './polyfills';
import './jquery';
import './bootstrap';
import './vue';
-import './gitlab_ui';
import '../lib/utils/axios_utils';
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 539d0d29e0d..bffc025ced3 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -5,6 +5,7 @@ import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
+import 'core-js/fn/object/values';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index 10548da8ec5..ea74fd27ff6 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -1,7 +1,11 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
deployKey: {
type: Object,
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 3589599986d..631a9673b3e 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -6,11 +6,13 @@ import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
components: {
KeysPanel,
NavigationTabs,
+ GlLoadingIcon,
},
props: {
endpoint: {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 59680959bb1..7c60fb3da42 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -3,6 +3,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import createFlash from '~/flash';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
@@ -21,6 +22,7 @@ export default {
HiddenFilesWarning,
CommitWidget,
TreeList,
+ GlLoadingIcon,
},
props: {
endpoint: {
@@ -223,7 +225,10 @@ export default {
:commit="commit"
/>
- <div class="files d-flex prepend-top-default">
+ <div
+ :data-can-create-note="getNoteableData.current_user.can_create_note"
+ class="files d-flex prepend-top-default"
+ >
<div
v-show="showTreeList"
class="diff-tree-list"
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 29b5aff0fb1..a5b87dfc2d9 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -36,7 +36,7 @@ export default {
},
computed: {
...mapState('diffs', ['commit', 'showTreeList']),
- ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'hasCollapsedFile']),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
@@ -113,8 +113,8 @@ export default {
class="inline-parallel-buttons d-none d-md-flex ml-auto"
>
<a
- v-if="areAllFilesCollapsed"
- class="btn btn-default"
+ v-show="hasCollapsedFile"
+ class="btn btn-default append-right-8"
@click="expandAllFiles"
>
{{ __('Expand all') }}
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index fb5556e3cd7..547742a5ff4 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -1,15 +1,22 @@
<script>
-import { mapGetters, mapState } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
-import { diffModes } from '~/ide/constants';
import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue';
+import NoteForm from '../../notes/components/note_form.vue';
+import ImageDiffOverlay from './image_diff_overlay.vue';
+import DiffDiscussions from './diff_discussions.vue';
+import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
+import { getDiffMode } from '../store/utils';
export default {
components: {
InlineDiffView,
ParallelDiffView,
DiffViewer,
+ NoteForm,
+ DiffDiscussions,
+ ImageDiffOverlay,
},
props: {
diffFile: {
@@ -23,13 +30,38 @@ export default {
endpoint: state => state.diffs.endpoint,
}),
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
+ ...mapGetters('diffs', ['getCommentFormForDiffFile']),
+ ...mapGetters(['getNoteableData', 'noteableType']),
diffMode() {
- const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]);
- return diffModes[diffModeKey] || diffModes.replaced;
+ return getDiffMode(this.diffFile);
},
isTextFile() {
return this.diffFile.viewer.name === 'text';
},
+ diffFileCommentForm() {
+ return this.getCommentFormForDiffFile(this.diffFile.fileHash);
+ },
+ showNotesContainer() {
+ return this.diffFile.discussions.length || this.diffFileCommentForm;
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['saveDiffDiscussion', 'closeDiffFileCommentForm']),
+ handleSaveNote(note) {
+ this.saveDiffDiscussion({
+ note,
+ formData: {
+ noteableData: this.getNoteableData,
+ noteableType: this.noteableType,
+ diffFile: this.diffFile,
+ positionType: IMAGE_DIFF_POSITION_TYPE,
+ x: this.diffFileCommentForm.x,
+ y: this.diffFileCommentForm.y,
+ width: this.diffFileCommentForm.width,
+ height: this.diffFileCommentForm.height,
+ },
+ });
+ },
},
};
</script>
@@ -56,7 +88,37 @@ export default {
:new-sha="diffFile.diffRefs.headSha"
:old-path="diffFile.oldPath"
:old-sha="diffFile.diffRefs.baseSha"
- :project-path="projectPath"/>
+ :file-hash="diffFile.fileHash"
+ :project-path="projectPath"
+ >
+ <image-diff-overlay
+ slot="image-overlay"
+ :discussions="diffFile.discussions"
+ :file-hash="diffFile.fileHash"
+ :can-comment="getNoteableData.current_user.can_create_note"
+ />
+ <div
+ v-if="showNotesContainer"
+ class="note-container"
+ >
+ <diff-discussions
+ v-if="diffFile.discussions.length"
+ class="diff-file-discussions"
+ :discussions="diffFile.discussions"
+ :should-collapse-discussions="true"
+ :render-avatar-badge="true"
+ />
+ <note-form
+ v-if="diffFileCommentForm"
+ ref="noteForm"
+ :is-editing="false"
+ :save-button-title="__('Comment')"
+ class="diff-comment-form new-note discussion-form discussion-form-container"
+ @handleFormUpdate="handleSaveNote"
+ @cancelForm="closeDiffFileCommentForm(diffFile.fileHash)"
+ />
+ </div>
+ </diff-viewer>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index cddbe554fbd..b9de487a737 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -1,24 +1,40 @@
<script>
import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default {
components: {
noteableDiscussion,
+ Icon,
},
props: {
discussions: {
type: Array,
required: true,
},
+ shouldCollapseDiscussions: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ renderAvatarBadge: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
methods: {
+ ...mapActions(['toggleDiscussion']),
...mapActions('diffs', ['removeDiscussionsFromDiff']),
deleteNoteHandler(discussion) {
if (discussion.notes.length <= 1) {
this.removeDiscussionsFromDiff(discussion);
}
},
+ isExpanded(discussion) {
+ return this.shouldCollapseDiscussions ? discussion.expanded : true;
+ },
},
};
</script>
@@ -26,22 +42,53 @@ export default {
<template>
<div>
<div
- v-for="discussion in discussions"
+ v-for="(discussion, index) in discussions"
:key="discussion.id"
- class="discussion-notes diff-discussions"
+ :class="{
+ collapsed: !isExpanded(discussion)
+ }"
+ class="discussion-notes diff-discussions position-relative"
>
<ul
:data-discussion-id="discussion.id"
class="notes"
>
+ <template v-if="shouldCollapseDiscussions">
+ <button
+ :class="{
+ 'diff-notes-collapse': discussion.expanded,
+ 'btn-transparent badge badge-pill': !discussion.expanded
+ }"
+ type="button"
+ class="js-diff-notes-toggle"
+ @click="toggleDiscussion({ discussionId: discussion.id })"
+ >
+ <icon
+ v-if="discussion.expanded"
+ name="collapse"
+ class="collapse-icon"
+ />
+ <template v-else>
+ {{ index + 1 }}
+ </template>
+ </button>
+ </template>
<noteable-discussion
+ v-show="isExpanded(discussion)"
:discussion="discussion"
- :render-header="false"
:render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true"
@noteDeleted="deleteNoteHandler"
- />
+ >
+ <span
+ v-if="renderAvatarBadge"
+ slot="avatar-badge"
+ class="badge badge-pill"
+ >
+ {{ index + 1 }}
+ </span>
+ </noteable-discussion>
</ul>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 958e57c5652..e76c7afd863 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
@@ -10,6 +11,7 @@ export default {
components: {
DiffFileHeader,
DiffContent,
+ GlLoadingIcon,
},
props: {
file: {
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 1b59777f901..254bc235691 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize, truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
export default {
@@ -10,6 +11,9 @@ export default {
Icon,
UserAvatarImage,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
discussions: {
type: Array,
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index 6eff3013dcd..f4a9be19496 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -167,7 +167,7 @@ export default {
<button
v-if="shouldShowCommentButton"
type="button"
- class="add-diff-note js-add-diff-note-button"
+ class="add-diff-note js-add-diff-note-button qa-diff-comment"
title="Add a comment to this line"
@click="handleCommentButton"
>
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
new file mode 100644
index 00000000000..ae1b0a52901
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -0,0 +1,139 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import _ from 'underscore';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ name: 'ImageDiffOverlay',
+ components: {
+ Icon,
+ },
+ props: {
+ discussions: {
+ type: [Array, Object],
+ required: true,
+ },
+ fileHash: {
+ type: String,
+ required: true,
+ },
+ canComment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showCommentIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ badgeClass: {
+ type: String,
+ required: false,
+ default: 'badge badge-pill',
+ },
+ shouldToggleDiscussion: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ ...mapGetters('diffs', ['getDiffFileByHash', 'getCommentFormForDiffFile']),
+ currentCommentForm() {
+ return this.getCommentFormForDiffFile(this.fileHash);
+ },
+ allDiscussions() {
+ return _.isArray(this.discussions) ? this.discussions : [this.discussions];
+ },
+ },
+ methods: {
+ ...mapActions(['toggleDiscussion']),
+ ...mapActions('diffs', ['openDiffFileCommentForm']),
+ getImageDimensions() {
+ return {
+ width: this.$parent.width,
+ height: this.$parent.height,
+ };
+ },
+ getPositionForObject(meta) {
+ const { x, y, width, height } = meta;
+ const imageWidth = this.getImageDimensions().width;
+ const imageHeight = this.getImageDimensions().height;
+ const widthRatio = imageWidth / width;
+ const heightRatio = imageHeight / height;
+
+ return {
+ x: Math.round(x * widthRatio),
+ y: Math.round(y * heightRatio),
+ };
+ },
+ getPosition(discussion) {
+ const { x, y } = this.getPositionForObject(discussion.position);
+
+ return {
+ left: `${x}px`,
+ top: `${y}px`,
+ };
+ },
+ clickedImage(x, y) {
+ const { width, height } = this.getImageDimensions();
+
+ this.openDiffFileCommentForm({
+ fileHash: this.fileHash,
+ width,
+ height,
+ x,
+ y,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="position-absolute w-100 h-100 image-diff-overlay">
+ <button
+ v-if="canComment"
+ type="button"
+ class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
+ @click="clickedImage($event.offsetX, $event.offsetY)"
+ >
+ <span class="sr-only">
+ {{ __('Add image comment') }}
+ </span>
+ </button>
+ <button
+ v-for="(discussion, index) in allDiscussions"
+ :key="discussion.id"
+ :style="getPosition(discussion)"
+ :class="badgeClass"
+ :disabled="!shouldToggleDiscussion"
+ class="js-image-badge"
+ type="button"
+ @click="toggleDiscussion({ discussionId: discussion.id })"
+ >
+ <icon
+ v-if="showCommentIcon"
+ name="image-comment-dark"
+ />
+ <template v-else>
+ {{ index + 1 }}
+ </template>
+ </button>
+ <button
+ v-if="currentCommentForm"
+ :style="{
+ left: `${currentCommentForm.x}px`,
+ top: `${currentCommentForm.y}px`
+ }"
+ :aria-label="__('Comment form position')"
+ class="btn-transparent comment-indicator"
+ type="button"
+ >
+ <icon
+ name="image-comment-dark"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 62fa34e835a..542acd3d930 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -102,7 +102,7 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isHover"
- class="diff-line-num new_line"
+ class="diff-line-num new_line qa-new-diff-line"
/>
<td
:class="line.type"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index 3339c56cbb6..3b71c0a1fd4 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -76,8 +76,9 @@ export default {
:class="className"
class="notes_holder"
>
- <td class="notes_line old"></td>
- <td class="notes_content parallel old">
+ <td
+ class="notes_content parallel old"
+ colspan="2">
<div
v-if="shouldRenderDiscussionsOnLeft"
class="content"
@@ -95,8 +96,9 @@ export default {
line-position="left"
/>
</td>
- <td class="notes_line new"></td>
- <td class="notes_content parallel new">
+ <td
+ class="notes_content parallel new"
+ colspan="2">
<div
v-if="shouldRenderDiscussionsOnRight"
class="content"
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 96e7bd63183..91052b303a6 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -18,8 +18,8 @@ export default {
},
data() {
const treeListStored = localStorage.getItem(treeListStorageKey);
- const renderTreeList = treeListStored !== null ?
- convertPermissionToBoolean(treeListStored) : true;
+ const renderTreeList =
+ treeListStored !== null ? convertPermissionToBoolean(treeListStored) : true;
return {
search: '',
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 6a50d2c1426..78a39baa4cb 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -12,6 +12,7 @@ export const NOTE_TYPE = 'Note';
export const NEW_LINE_TYPE = 'new';
export const OLD_LINE_TYPE = 'old';
export const TEXT_DIFF_POSITION_TYPE = 'text';
+export const IMAGE_DIFF_POSITION_TYPE = 'image';
export const LINE_POSITION_LEFT = 'left';
export const LINE_POSITION_RIGHT = 'right';
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index ca8ae605cb4..d3e9c7c88f0 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -50,8 +50,8 @@ export const assignDiscussionsToDiff = (
};
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
- const { fileHash, line_code } = removeDiscussion;
- commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code });
+ const { fileHash, line_code, id } = removeDiscussion;
+ commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code, id });
};
export const startRenderDiffsQueue = ({ state, commit }) => {
@@ -189,6 +189,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
+ .then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.fileHash))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
@@ -210,5 +211,19 @@ export const toggleShowTreeList = ({ commit, state }) => {
localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
};
+export const openDiffFileCommentForm = ({ commit, getters }, formData) => {
+ const form = getters.getCommentFormForDiffFile(formData.fileHash);
+
+ if (form) {
+ commit(types.UPDATE_DIFF_FILE_COMMENT_FORM, formData);
+ } else {
+ commit(types.OPEN_DIFF_FILE_COMMENT_FORM, formData);
+ }
+};
+
+export const closeDiffFileCommentForm = ({ commit }, fileHash) => {
+ commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index d4c205882ff..bf490f9d78a 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -5,7 +5,7 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
-export const areAllFilesCollapsed = state => state.diffFiles.every(file => file.collapsed);
+export const hasCollapsedFile = state => state.diffFiles.some(file => file.collapsed);
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
@@ -114,5 +114,8 @@ export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.
export const diffFilesLength = state => state.diffFiles.length;
+export const getCommentFormForDiffFile = state => fileHash =>
+ state.commentForms.find(form => form.fileHash === fileHash);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 1c5c35071de..085e255f1d3 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -24,4 +24,6 @@ export default () => ({
showTreeList:
storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : storedTreeShow === 'true',
currentDiffFileId: '',
+ projectPath: '',
+ commentForms: [],
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 6474ee628e2..e011031e72c 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -14,3 +14,7 @@ export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FIL
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
+
+export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
+export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
+export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 38a65f111a2..a7eea2c1449 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -153,20 +153,22 @@ export default {
});
},
- [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
+ [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode, id }) {
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
if (selectedFile) {
- const targetLine = selectedFile.parallelDiffLines.find(
- line =>
- (line.left && line.left.lineCode === lineCode) ||
- (line.right && line.right.lineCode === lineCode),
- );
- if (targetLine) {
- const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
-
- Object.assign(targetLine[side], {
- discussions: [],
- });
+ if (selectedFile.parallelDiffLines) {
+ const targetLine = selectedFile.parallelDiffLines.find(
+ line =>
+ (line.left && line.left.lineCode === lineCode) ||
+ (line.right && line.right.lineCode === lineCode),
+ );
+ if (targetLine) {
+ const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
+
+ Object.assign(targetLine[side], {
+ discussions: [],
+ });
+ }
}
if (selectedFile.highlightedDiffLines) {
@@ -180,6 +182,12 @@ export default {
});
}
}
+
+ if (selectedFile.discussions && selectedFile.discussions.length) {
+ selectedFile.discussions = selectedFile.discussions.filter(
+ discussion => discussion.id !== id,
+ );
+ }
}
},
[types.TOGGLE_FOLDER_OPEN](state, path) {
@@ -191,4 +199,25 @@ export default {
[types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
state.currentDiffFileId = fileId;
},
+ [types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
+ state.commentForms.push({
+ ...formData,
+ });
+ },
+ [types.UPDATE_DIFF_FILE_COMMENT_FORM](state, formData) {
+ const { fileHash } = formData;
+
+ state.commentForms = state.commentForms.map(form => {
+ if (form.fileHash === fileHash) {
+ return {
+ ...formData,
+ };
+ }
+
+ return form;
+ });
+ },
+ [types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) {
+ state.commentForms = state.commentForms.filter(form => form.fileHash !== fileHash);
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index a482a2b82c0..a935b9b1ffa 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -1,5 +1,6 @@
import _ from 'underscore';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { diffModes } from '~/ide/constants';
import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
@@ -34,6 +35,7 @@ export function getFormData(params) {
noteTargetLine,
diffViewType,
linePosition,
+ positionType,
} = params;
const position = JSON.stringify({
@@ -42,9 +44,13 @@ export function getFormData(params) {
head_sha: diffFile.diffRefs.headSha,
old_path: diffFile.oldPath,
new_path: diffFile.newPath,
- position_type: TEXT_DIFF_POSITION_TYPE,
- old_line: noteTargetLine.oldLine,
- new_line: noteTargetLine.newLine,
+ position_type: positionType || TEXT_DIFF_POSITION_TYPE,
+ old_line: noteTargetLine ? noteTargetLine.oldLine : null,
+ new_line: noteTargetLine ? noteTargetLine.newLine : null,
+ x: params.x,
+ y: params.y,
+ width: params.width,
+ height: params.height,
});
const postData = {
@@ -66,7 +72,7 @@ export function getFormData(params) {
diffFile.diffRefs.startSha && diffFile.diffRefs.headSha
? DIFF_NOTE_TYPE
: LEGACY_DIFF_NOTE_TYPE,
- line_code: noteTargetLine.lineCode,
+ line_code: noteTargetLine ? noteTargetLine.lineCode : null,
},
};
@@ -225,6 +231,7 @@ export function prepareDiffData(diffData) {
Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
+ discussions: [],
});
}
}
@@ -320,3 +327,8 @@ export const generateTreeList = files =>
},
{ treeEntries: {}, tree: [] },
);
+
+export const getDiffMode = diffFile => {
+ const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}File`]);
+ return diffModes[diffModeKey] || diffModes.replaced;
+};
diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
index 5bea47f23c5..d8d0fa1fac4 100644
--- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js
+++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
@@ -31,7 +31,7 @@ class DirtySubmitForm {
updateDirtyInput(event) {
const input = event.target;
- if (!input.dataset.dirtySubmitOriginalValue) return;
+ if (!input.dataset.isDirtySubmitInput) return;
this.updateDirtyInputs(input);
this.toggleSubmission();
@@ -65,6 +65,7 @@ class DirtySubmitForm {
}
static initInput(element) {
+ element.dataset.isDirtySubmitInput = true;
element.dataset.dirtySubmitOriginalValue = DirtySubmitForm.inputCurrentValue(element);
}
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 00d197d294f..a48f5fcb7d6 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
@@ -6,6 +7,7 @@ export default {
components: {
environmentTable,
tablePagination,
+ GlLoadingIcon,
},
props: {
isLoading: {
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 2bc168a6b02..03c3ad0401f 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,7 +1,10 @@
<script>
+import { s__, sprintf } from '~/locale';
+import { formatTime } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
directives: {
@@ -9,6 +12,7 @@ export default {
},
components: {
Icon,
+ GlLoadingIcon,
},
props: {
actions: {
@@ -28,10 +32,24 @@ export default {
},
},
methods: {
- onClickAction(endpoint) {
+ onClickAction(action) {
+ if (action.scheduledAt) {
+ const confirmationMessage = sprintf(
+ s__(
+ "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
+ ),
+ { jobName: action.name },
+ );
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/52156
+ // eslint-disable-next-line no-alert
+ if (!window.confirm(confirmationMessage)) {
+ return;
+ }
+ }
+
this.isLoading = true;
- eventHub.$emit('postAction', { endpoint });
+ eventHub.$emit('postAction', { endpoint: action.playPath });
},
isActionDisabled(action) {
@@ -41,6 +59,11 @@ export default {
return !action.playable;
},
+
+ remainingTime(action) {
+ const remainingMilliseconds = new Date(action.scheduledAt).getTime() - Date.now();
+ return formatTime(Math.max(0, remainingMilliseconds));
+ },
},
};
</script>
@@ -54,7 +77,7 @@ export default {
:aria-label="title"
:disabled="isLoading"
type="button"
- class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
+ class="dropdown btn btn-default dropdown-new js-environment-actions-dropdown"
data-container="body"
data-toggle="dropdown"
>
@@ -75,12 +98,19 @@ export default {
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
type="button"
- class="js-manual-action-link no-btn btn"
- @click="onClickAction(action.play_path)"
+ class="js-manual-action-link no-btn btn d-flex align-items-center"
+ @click="onClickAction(action)"
>
- <span>
+ <span class="flex-fill">
{{ action.name }}
</span>
+ <span
+ v-if="action.scheduledAt"
+ class="text-secondary"
+ >
+ <icon name="clock" />
+ {{ remainingTime(action) }}
+ </span>
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index 7446196de13..1e8a892c0b8 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
-import { s__ } from '../../locale';
+import { s__ } from '~/locale';
/**
* Renders the external url link in environments table.
@@ -11,7 +11,7 @@ export default {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
externalUrl: {
@@ -28,12 +28,11 @@ export default {
</script>
<template>
<a
- v-tooltip
+ v-gl-tooltip
:title="title"
:aria-label="title"
:href="externalUrl"
class="btn external-url"
- data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index bb9c139727e..50b0e9747ee 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,7 +1,7 @@
<script>
import Timeago from 'timeago.js';
import _ from 'underscore';
-import tooltip from '~/vue_shared/directives/tooltip';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
@@ -13,9 +13,10 @@ import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
/**
- * Envrionment Item Component
+ * Environment Item Component
*
* Renders a table row for each environment.
*/
@@ -35,7 +36,7 @@ export default {
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
@@ -60,7 +61,7 @@ export default {
computed: {
/**
- * Verifies if `last_deployment` key exists in the current Envrionment.
+ * Verifies if `last_deployment` key exists in the current Environment.
* This key is required to render most of the html - this method works has
* an helper.
*
@@ -74,21 +75,6 @@ export default {
},
/**
- * Verifies is the given environment has manual actions.
- * Used to verify if we should render them or nor.
- *
- * @returns {Boolean|Undefined}
- */
- hasManualActions() {
- return (
- this.model &&
- this.model.last_deployment &&
- this.model.last_deployment.manual_actions &&
- this.model.last_deployment.manual_actions.length > 0
- );
- },
-
- /**
* Checkes whether the environment is protected.
* (`is_protected` currently only set in EE)
*
@@ -154,23 +140,20 @@ export default {
return '';
},
- /**
- * Returns the manual actions with the name parsed.
- *
- * @returns {Array.<Object>|Undefined}
- */
- manualActions() {
- if (this.hasManualActions) {
- return this.model.last_deployment.manual_actions.map(action => {
- const parsedAction = {
- name: humanize(action.name),
- play_path: action.play_path,
- playable: action.playable,
- };
- return parsedAction;
- });
+ actions() {
+ if (!this.model || !this.model.last_deployment || !this.canCreateDeployment) {
+ return [];
}
- return [];
+
+ const { manualActions, scheduledActions } = convertObjectPropsToCamelCase(
+ this.model.last_deployment,
+ { deep: true },
+ );
+ const combinedActions = (manualActions || []).concat(scheduledActions || []);
+ return combinedActions.map(action => ({
+ ...action,
+ name: humanize(action.name),
+ }));
},
/**
@@ -443,7 +426,7 @@ export default {
displayEnvironmentActions() {
return (
- this.hasManualActions ||
+ this.actions.length > 0 ||
this.externalURL ||
this.monitoringUrl ||
this.canStopEnvironment ||
@@ -472,7 +455,7 @@ export default {
class="gl-responsive-table-row"
role="row">
<div
- v-tooltip
+ v-gl-tooltip
:title="model.name"
class="table-section section-wrap section-15 text-truncate"
role="gridcell"
@@ -619,8 +602,8 @@ export default {
/>
<actions-component
- v-if="hasManualActions && canCreateDeployment"
- :actions="manualActions"
+ v-if="actions.length > 0"
+ :actions="actions"
/>
<terminal-button-component
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 26bec125445..7c723fa8979 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,9 +2,8 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
-import { GlButton } from '@gitlab-org/gitlab-ui';
+import { GlButton, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
@@ -12,7 +11,7 @@ export default {
GlButton,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
monitoringUrl: {
@@ -29,12 +28,11 @@ export default {
</script>
<template>
<gl-button
- v-tooltip
+ v-gl-tooltip
:href="monitoringUrl"
:title="title"
:aria-label="title"
class="monitoring-url d-none d-sm-none d-md-block"
- data-container="body"
rel="noopener noreferrer nofollow"
variant="default"
>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 9e137f79dcc..298469e6482 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -6,19 +6,18 @@
* Makes a post request when the button is clicked.
*/
import { s__ } from '~/locale';
+import { GlTooltipDirective, GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../event_hub';
export default {
components: {
Icon,
+ GlLoadingIcon,
},
-
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
-
props: {
retryUrl: {
type: String,
@@ -55,21 +54,21 @@ export default {
</script>
<template>
<button
- v-tooltip
+ v-gl-tooltip
:disabled="isLoading"
:title="title"
type="button"
class="btn d-none d-sm-none d-md-block"
@click="onClick"
>
-
<icon
v-if="isLastDeployment"
- name="repeat" />
+ name="repeat"
+ />
<icon
v-else
- name="redo"/>
-
+ name="redo"
+ />
<gl-loading-icon v-if="isLoading" />
</button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index a814b3405f5..327c96a93e9 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -5,49 +5,42 @@
*/
import $ from 'jquery';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
LoadingButton,
},
-
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
-
props: {
environment: {
type: Object,
required: true,
},
},
-
data() {
return {
isLoading: false,
};
},
-
computed: {
title() {
return s__('Environments|Stop environment');
},
},
-
mounted() {
eventHub.$on('stopEnvironment', this.onStopEnvironment);
},
-
beforeDestroy() {
eventHub.$off('stopEnvironment', this.onStopEnvironment);
},
-
methods: {
onClick() {
$(this.$el).tooltip('dispose');
@@ -63,12 +56,11 @@ export default {
</script>
<template>
<loading-button
- v-tooltip
+ v-gl-tooltip
:loading="isLoading"
:title="title"
:aria-label="title"
container-class="btn btn-danger d-none d-sm-none d-md-block"
- data-container="body"
data-toggle="modal"
data-target="#stop-environment-modal"
@click="onClick"
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 350417e5ad0..b8b909f350c 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -3,15 +3,15 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
terminalPath: {
@@ -29,12 +29,11 @@ export default {
</script>
<template>
<a
- v-tooltip
+ v-gl-tooltip
:title="title"
:aria-label="title"
:href="terminalPath"
class="btn terminal-button d-none d-sm-none d-md-block"
- data-container="body"
>
<icon name="terminal" />
</a>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index e2ecf426e64..557b2062c64 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,94 +1,92 @@
<script>
- import Flash from '../../flash';
- import { s__ } from '../../locale';
- import emptyState from './empty_state.vue';
- import eventHub from '../event_hub';
- import environmentsMixin from '../mixins/environments_mixin';
- import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
- import StopEnvironmentModal from './stop_environment_modal.vue';
+import Flash from '../../flash';
+import { s__ } from '../../locale';
+import emptyState from './empty_state.vue';
+import eventHub from '../event_hub';
+import environmentsMixin from '../mixins/environments_mixin';
+import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+import StopEnvironmentModal from './stop_environment_modal.vue';
- export default {
- components: {
- emptyState,
- StopEnvironmentModal,
- },
+export default {
+ components: {
+ emptyState,
+ StopEnvironmentModal,
+ },
- mixins: [
- CIPaginationMixin,
- environmentsMixin,
- ],
+ mixins: [CIPaginationMixin, environmentsMixin],
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- canCreateEnvironment: {
- type: Boolean,
- required: true,
- },
- canCreateDeployment: {
- type: Boolean,
- required: true,
- },
- canReadEnvironment: {
- type: Boolean,
- required: true,
- },
- cssContainerClass: {
- type: String,
- required: true,
- },
- newEnvironmentPath: {
- type: String,
- required: true,
- },
- helpPagePath: {
- type: String,
- required: true,
- },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
-
- created() {
- eventHub.$on('toggleFolder', this.toggleFolder);
+ canCreateEnvironment: {
+ type: Boolean,
+ required: true,
},
-
- beforeDestroy() {
- eventHub.$off('toggleFolder');
+ canCreateDeployment: {
+ type: Boolean,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ cssContainerClass: {
+ type: String,
+ required: true,
+ },
+ newEnvironmentPath: {
+ type: String,
+ required: true,
},
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+
+ created() {
+ eventHub.$on('toggleFolder', this.toggleFolder);
+ },
- methods: {
- toggleFolder(folder) {
- this.store.toggleFolder(folder);
+ beforeDestroy() {
+ eventHub.$off('toggleFolder');
+ },
- if (!folder.isOpen) {
- this.fetchChildEnvironments(folder, true);
- }
- },
+ methods: {
+ toggleFolder(folder) {
+ this.store.toggleFolder(folder);
- fetchChildEnvironments(folder, showLoader = false) {
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
+ if (!folder.isOpen) {
+ this.fetchChildEnvironments(folder, true);
+ }
+ },
- this.service.getFolderContent(folder.folder_path)
- .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.'));
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
- });
- },
+ fetchChildEnvironments(folder, showLoader = false) {
+ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
+
+ this.service
+ .getFolderContent(folder.folder_path)
+ .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.'));
+ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
+ });
+ },
- successCallback(resp) {
- this.saveData(resp);
+ successCallback(resp) {
+ this.saveData(resp);
- // We need to verify if any folder is open to also update it
- const openFolders = this.store.getOpenFolders();
- if (openFolders.length) {
- openFolders.forEach(folder => this.fetchChildEnvironments(folder));
- }
- },
+ // We need to verify if any folder is open to also update it
+ const openFolders = this.store.getOpenFolders();
+ if (openFolders.length) {
+ openFolders.forEach(folder => this.fetchChildEnvironments(folder));
+ }
},
- };
+ },
+};
</script>
<template>
<div :class="cssContainerClass">
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 16abafebbc0..c03d4f29ff9 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -2,11 +2,13 @@
/**
* Render environments table.
*/
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import environmentItem from './environment_item.vue';
export default {
components: {
environmentItem,
+ GlLoadingIcon,
},
props: {
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index 657cc8cd1aa..6397f6caf1b 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
@@ -15,7 +15,7 @@ export default {
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
@@ -67,7 +67,7 @@ export default {
>
Stopping
<span
- v-tooltip
+ v-gl-tooltip
:title="environment.name"
class="text-truncate ml-1 mr-1 flex-fill"
>{{ environment.name }}</span>
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5ce9225a4bb..5808a2d4afa 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -34,14 +34,14 @@ export default class EnvironmentsStore {
* @returns {Array}
*/
storeEnvironments(environments = []) {
- const filteredEnvironments = environments.map((env) => {
- const oldEnvironmentState = this.state.environments
- .find((element) => {
- if (env.latest) {
- return element.id === env.latest.id;
- }
- return element.id === env.id;
- }) || {};
+ const filteredEnvironments = environments.map(env => {
+ const oldEnvironmentState =
+ this.state.environments.find(element => {
+ if (env.latest) {
+ return element.id === env.latest.id;
+ }
+ return element.id === env.id;
+ }) || {};
let filtered = {};
@@ -101,11 +101,11 @@ export default class EnvironmentsStore {
}
/**
- * Toggles folder open property for the given folder.
- *
- * @param {Object} folder
- * @return {Array}
- */
+ * Toggles folder open property for the given folder.
+ *
+ * @param {Object} folder
+ * @return {Array}
+ */
toggleFolder(folder) {
return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen);
}
@@ -119,7 +119,7 @@ export default class EnvironmentsStore {
* @return {Object}
*/
setfolderContent(folder, environments) {
- const updatedEnvironments = environments.map((env) => {
+ const updatedEnvironments = environments.map(env => {
let updated = env;
if (env.latest) {
@@ -148,7 +148,7 @@ export default class EnvironmentsStore {
updateEnvironmentProp(environment, prop, newValue) {
const { environments } = this.state;
- const updatedEnvironments = environments.map((env) => {
+ const updatedEnvironments = environments.map(env => {
const updateEnv = Object.assign({}, env);
if (env.id === environment.id) {
updateEnv[prop] = newValue;
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index d36f38a70b5..d5027590bb7 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -39,8 +39,9 @@ export default class DropdownUser extends FilteredSearchDropdown {
}
itemClicked(e) {
- super.itemClicked(e,
- selected => selected.querySelector('.dropdown-light-content').innerText.trim());
+ super.itemClicked(e, selected =>
+ selected.querySelector('.dropdown-light-content').innerText.trim(),
+ );
}
renderContent(forceShowList = false) {
@@ -68,7 +69,7 @@ export default class DropdownUser extends FilteredSearchDropdown {
// Removes the first character if it is a quotation so that we can search
// with multiple words
- if (value[0] === '"' || value[0] === '\'') {
+ if (value[0] === '"' || value[0] === "'") {
value = value.slice(1);
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 4eb67ff7649..146d3ba963c 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -85,7 +85,7 @@ export default class FilteredSearchDropdown {
}
dispatchInputEvent() {
- // Propogate input change to FilteredSearchDropdownManager
+ // Propagate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open
this.input.dispatchEvent(
new CustomEvent('input', {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index cd3d532c958..57ec6603d80 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -108,7 +108,7 @@ export default class FilteredSearchDropdownManager {
},
};
- supportedTokens.forEach((type) => {
+ supportedTokens.forEach(type => {
if (availableMappings[type]) {
allowedMappings[type] = availableMappings[type];
}
@@ -142,10 +142,7 @@ export default class FilteredSearchDropdownManager {
}
static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) {
- const {
- uppercaseTokenName = false,
- capitalizeTokenValue = false,
- } = options;
+ const { uppercaseTokenName = false, capitalizeTokenValue = false } = options;
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, {
uppercaseTokenName,
@@ -164,13 +161,16 @@ export default class FilteredSearchDropdownManager {
updateDropdownOffset(key) {
// Always align dropdown with the input field
- let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
+ let offset =
+ this.filteredSearchInput.getBoundingClientRect().left -
+ this.container.querySelector('.scroll-container').getBoundingClientRect().left;
const maxInputWidth = 240;
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
// Make sure offset never exceeds the input container
- const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
+ const offsetMaxWidth =
+ this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) {
offset = offsetMaxWidth;
}
@@ -196,8 +196,7 @@ export default class FilteredSearchDropdownManager {
const glArguments = Object.assign({}, defaultArguments, extraArguments);
// Passing glArguments to `new glClass(<arguments>)`
- mappingKey.reference =
- new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
+ mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
}
if (firstLoad) {
@@ -224,8 +223,8 @@ export default class FilteredSearchDropdownManager {
}
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
- const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
- && this.mapping[match.key];
+ const shouldOpenFilterDropdown =
+ match && this.currentDropdown !== match.key && this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
@@ -236,8 +235,10 @@ export default class FilteredSearchDropdownManager {
setDropdown() {
const query = DropdownUtils.getSearchQuery(true);
- const { lastToken, searchToken } =
- this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys());
+ const { lastToken, searchToken } = this.tokenizer.processTokens(
+ query,
+ this.filteredSearchTokenKeys.getKeys(),
+ );
if (this.currentDropdown) {
this.updateCurrentDropdownOffset();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 54533ebb70d..4a2af02b40a 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,8 +1,5 @@
import _ from 'underscore';
-import {
- getParameterByName,
- getUrlParamsArray,
-} from '~/lib/utils/common_utils';
+import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
@@ -48,24 +45,28 @@ export default class FilteredSearchManager {
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
allowedKeys: this.filteredSearchTokenKeys.getKeys(),
});
- this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
- const fullPath = this.searchHistoryDropdownElement ?
- this.searchHistoryDropdownElement.dataset.fullPath : 'project';
+ this.searchHistoryDropdownElement = document.querySelector(
+ '.js-filtered-search-history-dropdown',
+ );
+ const fullPath = this.searchHistoryDropdownElement
+ ? this.searchHistoryDropdownElement.dataset.fullPath
+ : 'project';
const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
}
setup() {
// Fetch recent searches from localStorage
- this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
- .catch((error) => {
+ this.fetchingRecentSearchesPromise = this.recentSearchesService
+ .fetch()
+ .catch(error => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
new Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array
return [];
})
- .then((searches) => {
+ .then(searches => {
if (!searches) {
return;
}
@@ -120,7 +121,7 @@ export default class FilteredSearchManager {
if (this.stateFilters) {
this.searchStateWrapper = this.searchState.bind(this);
- this.applyToStateFilters((filterEl) => {
+ this.applyToStateFilters(filterEl => {
filterEl.addEventListener('click', this.searchStateWrapper);
});
}
@@ -128,14 +129,14 @@ export default class FilteredSearchManager {
unbindStateEvents() {
if (this.stateFilters) {
- this.applyToStateFilters((filterEl) => {
+ this.applyToStateFilters(filterEl => {
filterEl.removeEventListener('click', this.searchStateWrapper);
});
}
}
applyToStateFilters(callback) {
- this.stateFilters.querySelectorAll('a[data-state]').forEach((filterEl) => {
+ this.stateFilters.querySelectorAll('a[data-state]').forEach(filterEl => {
if (this.states.indexOf(filterEl.dataset.state) > -1) {
callback(filterEl);
}
@@ -207,7 +208,7 @@ export default class FilteredSearchManager {
let backspaceCount = 0;
// closure for keeping track of the number of backspace keystrokes
- return (e) => {
+ return e => {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
@@ -274,8 +275,12 @@ export default class FilteredSearchManager {
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
- if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
- !isElementInStaticFilterDropdown && inputContainer) {
+ if (
+ !isElementInFilteredSearch &&
+ !isElementInDynamicFilterDropdown &&
+ !isElementInStaticFilterDropdown &&
+ inputContainer
+ ) {
inputContainer.classList.remove('focus');
}
}
@@ -368,7 +373,7 @@ export default class FilteredSearchManager {
const removeElements = [];
- [].forEach.call(this.tokensContainer.children, (t) => {
+ [].forEach.call(this.tokensContainer.children, t => {
let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) {
@@ -381,7 +386,7 @@ export default class FilteredSearchManager {
}
});
- removeElements.forEach((el) => {
+ removeElements.forEach(el => {
el.parentElement.removeChild(el);
});
@@ -397,13 +402,14 @@ export default class FilteredSearchManager {
handleInputVisualToken() {
const input = this.filteredSearchInput;
- const { tokens, searchToken }
- = this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys());
- const { isLastVisualTokenValid }
- = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const { tokens, searchToken } = this.tokenizer.processTokens(
+ input.value,
+ this.filteredSearchTokenKeys.getKeys(),
+ );
+ const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (isLastVisualTokenValid) {
- tokens.forEach((t) => {
+ tokens.forEach(t => {
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, {
uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key),
@@ -453,15 +459,17 @@ export default class FilteredSearchManager {
saveCurrentSearchQuery() {
// Don't save before we have fetched the already saved searches
- this.fetchingRecentSearchesPromise.then(() => {
- const searchQuery = DropdownUtils.getSearchQuery();
- if (searchQuery.length > 0) {
- const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
- this.recentSearchesService.save(resultantSearches);
- }
- }).catch(() => {
- // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
- });
+ this.fetchingRecentSearchesPromise
+ .then(() => {
+ const searchQuery = DropdownUtils.getSearchQuery();
+ if (searchQuery.length > 0) {
+ const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
+ this.recentSearchesService.save(resultantSearches);
+ }
+ })
+ .catch(() => {
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
+ });
}
// allows for modifying params array when a param can't be included in the URL (e.g. Service Desk)
@@ -475,7 +483,7 @@ export default class FilteredSearchManager {
const usernameParams = this.getUsernameParams();
let hasFilteredSearch = false;
- params.forEach((p) => {
+ params.forEach(p => {
const split = p.split('=');
const keyParam = decodeURIComponent(split[0]);
const value = split[1];
@@ -486,11 +494,9 @@ export default class FilteredSearchManager {
if (condition) {
hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(condition.tokenKey);
- FilteredSearchVisualTokens.addFilterVisualToken(
- condition.tokenKey,
- condition.value,
- { canEdit },
- );
+ FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value, {
+ canEdit,
+ });
} else {
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
@@ -510,7 +516,7 @@ export default class FilteredSearchManager {
if (sanitizedValue.indexOf(' ') !== -1) {
// Prefer ", but use ' if required
- quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
+ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : "'";
}
hasFilteredSearch = true;
@@ -531,7 +537,9 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'assignee';
const canEdit = this.canEdit && this.canEdit(tokenName);
- FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit });
+ FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, {
+ canEdit,
+ });
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
@@ -539,7 +547,9 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'author';
const canEdit = this.canEdit && this.canEdit(tokenName);
- FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit });
+ FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, {
+ canEdit,
+ });
}
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true;
@@ -580,9 +590,11 @@ export default class FilteredSearchManager {
const currentState = state || getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
- tokens.forEach((token) => {
- const condition = this.filteredSearchTokenKeys
- .searchByConditionKeyValue(token.key, token.value.toLowerCase());
+ tokens.forEach(token => {
+ const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue(
+ token.key,
+ token.value.toLowerCase(),
+ );
const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const { param } = tokenConfig;
@@ -601,8 +613,10 @@ export default class FilteredSearchManager {
tokenValue = tokenValue.toLowerCase();
}
- if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
- (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
+ if (
+ (tokenValue[0] === "'" && tokenValue[tokenValue.length - 1] === "'") ||
+ (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')
+ ) {
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
}
@@ -613,7 +627,10 @@ export default class FilteredSearchManager {
});
if (searchToken) {
- const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
+ const sanitized = searchToken
+ .split(' ')
+ .map(t => encodeURIComponent(t))
+ .join('+');
paths.push(`search=${sanitized}`);
}
@@ -630,7 +647,7 @@ export default class FilteredSearchManager {
const usernamesById = {};
try {
const attribute = this.filteredSearchInput.getAttribute('data-username-params');
- JSON.parse(attribute).forEach((user) => {
+ JSON.parse(attribute).forEach(user => {
usernamesById[user.id] = user.username;
});
} catch (e) {
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index e22f542b7bf..bb0ecb8efe7 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -68,12 +68,12 @@ export const conditions = [
value: 'any',
},
{
- url: 'milestone_title=No+Milestone',
+ url: 'milestone_title=None',
tokenKey: 'milestone',
value: 'none',
},
{
- url: 'milestone_title=Any+Milestone',
+ url: 'milestone_title=Any',
tokenKey: 'milestone',
value: 'any',
},
@@ -92,6 +92,16 @@ export const conditions = [
tokenKey: 'label',
value: 'none',
},
+ {
+ url: 'my_reaction_emoji=None',
+ tokenKey: 'my-reaction',
+ value: 'none',
+ },
+ {
+ url: 'my_reaction_emoji=Any',
+ tokenKey: 'my-reaction',
+ value: 'any',
+ },
];
const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys(
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 749c09f897c..c2397842125 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -40,7 +40,9 @@ const createFlashEl = (message, type, isFixedLayout = false) => `
class="flash-${type}"
>
<div
- class="flash-text ${isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''}"
+ class="flash-text ${
+ isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''
+ }"
>
${_.escape(message)}
</div>
@@ -78,7 +80,9 @@ const createFlash = function createFlash(
if (!flashContainer) return null;
- const isFixedLayout = navigation ? navigation.parentNode.classList.contains('container-limited') : true;
+ const isFixedLayout = navigation
+ ? navigation.parentNode.classList.contains('container-limited')
+ : true;
flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout);
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 70a8838b772..159c0bdc992 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -1,6 +1,7 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import AccessorUtilities from '~/lib/utils/accessor';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../event_hub';
import store from '../store/';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
@@ -14,6 +15,7 @@ export default {
components: {
FrequentItemsSearchInput,
FrequentItemsList,
+ GlLoadingIcon,
},
mixins: [frequentItemsMixin],
props: {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 7dd0efd622d..00b3d283570 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -94,7 +94,7 @@ class GfmAutoComplete {
...this.getDefaultCallbacks(),
beforeSave(commands) {
if (GfmAutoComplete.isLoading(commands)) return commands;
- return $.map(commands, (c) => {
+ return $.map(commands, c => {
let search = c.name;
if (c.aliases.length > 0) {
search = `${search} ${c.aliases.join(' ')}`;
@@ -167,7 +167,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(members) {
- return $.map(members, (m) => {
+ return $.map(members, m => {
let title = '';
if (m.username == null) {
return m;
@@ -178,7 +178,9 @@ class GfmAutoComplete {
}
const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
- const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
+ const imgAvatar = `<img src="${m.avatar_url}" alt="${
+ m.username
+ }" class="avatar avatar-inline center s26"/>`;
const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
return {
@@ -211,7 +213,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(issues) {
- return $.map(issues, (i) => {
+ return $.map(issues, i => {
if (i.title == null) {
return i;
}
@@ -244,7 +246,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(milestones) {
- return $.map(milestones, (m) => {
+ return $.map(milestones, m => {
if (m.title == null) {
return m;
}
@@ -277,7 +279,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(merges) {
- return $.map(merges, (m) => {
+ return $.map(merges, m => {
if (m.title == null) {
return m;
}
@@ -324,13 +326,20 @@ class GfmAutoComplete {
},
matcher(flag, subtext) {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
- const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext);
+ const subtextNodes = subtext
+ .split(/\n+/g)
+ .pop()
+ .split(GfmAutoComplete.regexSubtext);
// Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
- command = subtextNodes.find((node) => {
- if (node === LABEL_COMMAND.LABEL ||
- node === LABEL_COMMAND.RELABEL ||
- node === LABEL_COMMAND.UNLABEL) { return node; }
+ command = subtextNodes.find(node => {
+ if (
+ node === LABEL_COMMAND.LABEL ||
+ node === LABEL_COMMAND.RELABEL ||
+ node === LABEL_COMMAND.UNLABEL
+ ) {
+ return node;
+ }
return null;
});
@@ -380,7 +389,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(snippets) {
- return $.map(snippets, (m) => {
+ return $.map(snippets, m => {
if (m.title == null) {
return m;
}
@@ -458,13 +467,17 @@ class GfmAutoComplete {
this.loadData($input, at, validEmojiNames);
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
- .catch(() => { this.isLoadingData[at] = false; });
+ .catch(() => {
+ this.isLoadingData[at] = false;
+ });
} else if (dataSource) {
AjaxCache.retrieve(dataSource, true)
- .then((data) => {
+ .then(data => {
this.loadData($input, at, data);
})
- .catch(() => { this.isLoadingData[at] = false; });
+ .catch(() => {
+ this.isLoadingData[at] = false;
+ });
} else {
this.isLoadingData[at] = false;
}
@@ -497,15 +510,16 @@ class GfmAutoComplete {
}
const loadingState = GfmAutoComplete.defaultLoadingData[0];
- return dataToInspect &&
- (dataToInspect === loadingState || dataToInspect.name === loadingState);
+ return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState);
}
static defaultMatcher(flag, subtext, controllers) {
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
- const atSymbolsWithBar = Object.keys(controllers).join('|').replace(/[$]/, '\\$&');
+ const atSymbolsWithBar = Object.keys(controllers)
+ .join('|')
+ .replace(/[$]/, '\\$&');
const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
@@ -513,7 +527,10 @@ class GfmAutoComplete {
const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF');
- const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
+ const regexp = new RegExp(
+ `^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`,
+ 'gi',
+ );
return regexp.exec(targetSubtext);
}
@@ -552,8 +569,9 @@ GfmAutoComplete.Members = {
template: '<li>${avatarTag} ${username} <small>${title}</small></li>',
};
GfmAutoComplete.Labels = {
- // eslint-disable-next-line no-template-curly-in-string
- template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
+ template:
+ // eslint-disable-next-line no-template-curly-in-string
+ '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
};
// Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = {
@@ -567,7 +585,8 @@ GfmAutoComplete.Milestones = {
template: '<li>${title}</li>',
};
GfmAutoComplete.Loading = {
- template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>',
+ template:
+ '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>',
};
export default GfmAutoComplete;
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index 3764e7ab422..d5d5954ce6a 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -28,7 +28,7 @@ export default class GlFieldErrors {
this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit);
}
- /* Neccessary to prevent intercept and override invalid form submit
+ /* Necessary to prevent intercept and override invalid form submit
* because Safari & iOS quietly allow form submission when form is invalid
* and prevents disabling of invalid submit button by application.js */
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index a032f291546..2a4a39436e7 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -8,6 +8,7 @@ import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../event_hub';
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
import groupsComponent from './groups.vue';
@@ -16,6 +17,7 @@ export default {
components: {
DeprecatedModal,
groupsComponent,
+ GlLoadingIcon,
},
props: {
action: {
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
index 52ccc537c9d..358f1153de2 100644
--- a/app/assets/javascripts/ide/components/branches/search_list.vue
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -2,12 +2,14 @@
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Item from './item.vue';
export default {
components: {
Item,
Icon,
+ GlLoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index a20dc0a7006..2d9bd99e82a 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -1,7 +1,11 @@
<script>
import { mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
message: {
type: Object,
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
index 94222c08e91..891f7d48b4c 100644
--- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -2,10 +2,12 @@
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
components: {
DropdownButton,
+ GlLoadingIcon,
},
props: {
data: {
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index ad6151e3bf6..0a368f6558c 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -43,7 +43,7 @@ export default {
'currentProjectId',
'errorMessage',
]),
- ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']),
+ ...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
@@ -63,7 +63,7 @@ export default {
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
- if (!this.someUncommitedChanges) return undefined;
+ if (!this.someUncommittedChanges) return undefined;
Object.assign(e, {
returnValue,
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index d4c430cd2f3..364ab9426e0 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -25,11 +25,11 @@ export default {
},
computed: {
...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']),
- ...mapGetters(['currentProject', 'someUncommitedChanges']),
+ ...mapGetters(['currentProject', 'someUncommittedChanges']),
showSuccessMessage() {
return (
this.currentActivityView === activityBarViews.edit &&
- (this.lastCommitMsg && !this.someUncommitedChanges)
+ (this.lastCommitMsg && !this.someUncommittedChanges)
);
},
},
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
index acd37605d16..57da8b4e2cb 100644
--- a/app/assets/javascripts/ide/components/jobs/list.vue
+++ b/app/assets/javascripts/ide/components/jobs/list.vue
@@ -1,10 +1,12 @@
<script>
import { mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Stage from './stage.vue';
export default {
components: {
Stage,
+ GlLoadingIcon,
},
props: {
stages: {
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index ec168d36b9e..5644759d2f9 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
@@ -12,6 +13,7 @@ export default {
Icon,
CiIcon,
Item,
+ GlLoadingIcon,
},
props: {
stage: {
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index f5e42e87f1b..e4000f588bd 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -3,6 +3,7 @@ import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
@@ -16,6 +17,7 @@ export default {
TokenedInput,
Item,
Icon,
+ GlLoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index b670b0355b7..16aec1decd6 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { sprintf, __ } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
@@ -17,6 +18,7 @@ export default {
Tab,
JobsList,
EmptyState,
+ GlLoadingIcon,
},
computed: {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index 37a8ad36507..0bd56ff6e9b 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Navigator from './navigator.vue';
import { packageJsonPath } from '../../constants';
import { createPathWithExt } from '../../utils';
@@ -10,6 +11,7 @@ import { createPathWithExt } from '../../utils';
export default {
components: {
Navigator,
+ GlLoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
index 42f23801692..af8959186f9 100644
--- a/app/assets/javascripts/ide/components/preview/navigator.vue
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -1,10 +1,12 @@
<script>
import { listen } from 'codesandbox-api';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
components: {
Icon,
+ GlLoadingIcon,
},
props: {
manager: {
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index d3b24c5b793..5e86876c1c1 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -27,10 +27,10 @@ export default {
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']),
+ ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']),
...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() {
- return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
+ return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal);
},
activeFileKey() {
return this.activeFile ? this.activeFile.key : null;
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 709748fb530..8ad85074d6b 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -63,7 +63,7 @@ export const isEditModeActive = state => state.currentActivityView === activityB
export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
-export const someUncommitedChanges = state =>
+export const someUncommittedChanges = state =>
!!(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => {
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 17fd5321642..93c89411b4a 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -1,10 +1,12 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
TimeagoTooltip,
+ GlLink,
},
mixins: [timeagoMixin],
props: {
@@ -53,16 +55,16 @@ export default {
class="btn-group d-flex"
role="group"
>
- <a
+ <gl-link
v-if="artifact.keep_path"
:href="artifact.keep_path"
class="js-keep-artifacts btn btn-sm btn-default"
data-method="post"
>
{{ s__('Job|Keep') }}
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="artifact.download_path"
:href="artifact.download_path"
class="js-download-artifacts btn btn-sm btn-default"
@@ -70,15 +72,15 @@ export default {
rel="nofollow"
>
{{ s__('Job|Download') }}
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="artifact.browse_path"
:href="artifact.browse_path"
class="js-browse-artifacts btn btn-sm btn-default"
>
{{ s__('Job|Browse') }}
- </a>
+ </gl-link>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
index 7d51f6afd10..06fe23fedce 100644
--- a/app/assets/javascripts/jobs/components/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -1,9 +1,11 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
components: {
ClipboardButton,
+ GlLink,
},
props: {
commit: {
@@ -31,10 +33,10 @@ export default {
<p>
{{ __('Commit') }}
- <a
+ <gl-link
:href="commit.commit_path"
class="js-commit-sha commit-sha link-commit"
- >{{ commit.short_id }}</a>
+ >{{ commit.short_id }}</gl-link>
<clipboard-button
:text="commit.short_id"
@@ -42,11 +44,11 @@ export default {
css-class="btn btn-clipboard btn-transparent"
/>
- <a
+ <gl-link
v-if="mergeRequest"
:href="mergeRequest.path"
class="js-link-commit link-commit"
- >!{{ mergeRequest.iid }}</a>
+ >!{{ mergeRequest.iid }}</gl-link>
</p>
<p class="build-light-text append-bottom-0">
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index ee5ceb99b0a..be7425c2d25 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -1,5 +1,10 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
+
export default {
+ components: {
+ GlLink,
+ },
props: {
illustrationPath: {
type: String,
@@ -62,13 +67,13 @@ export default {
v-if="action"
class="text-center"
>
- <a
+ <gl-link
:href="action.path"
:data-method="action.method"
class="js-job-empty-state-action btn btn-primary"
>
{{ action.button_title }}
- </a>
+ </gl-link>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue
index 5ffbfb6e19a..d80e905c68e 100644
--- a/app/assets/javascripts/jobs/components/erased_block.vue
+++ b/app/assets/javascripts/jobs/components/erased_block.vue
@@ -1,10 +1,12 @@
<script>
import _ from 'underscore';
+import { GlLink } from '@gitlab-org/gitlab-ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
TimeagoTooltip,
+ GlLink,
},
props: {
user: {
@@ -29,9 +31,9 @@ export default {
<div class="erased alert alert-warning">
<template v-if="isErasedByUser">
{{ s__("Job|Job has been erased by") }}
- <a :href="user.web_url">
+ <gl-link :href="user.web_url">
{{ user.username }}
- </a>
+ </gl-link>
</template>
<template v-else>
{{ s__("Job|Job has been erased") }}
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index ac19034f69d..90216b04e92 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -1,164 +1,188 @@
<script>
- import _ from 'underscore';
- import { mapGetters, mapState, mapActions } from 'vuex';
- import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
- import bp from '~/breakpoints';
- import CiHeader from '~/vue_shared/components/header_ci_component.vue';
- import Callout from '~/vue_shared/components/callout.vue';
- import createStore from '../store';
- import EmptyState from './empty_state.vue';
- import EnvironmentsBlock from './environments_block.vue';
- import ErasedBlock from './erased_block.vue';
- import Log from './job_log.vue';
- import LogTopBar from './job_log_controllers.vue';
- import StuckBlock from './stuck_block.vue';
- import Sidebar from './sidebar.vue';
+import _ from 'underscore';
+import { mapGetters, mapState, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
+import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
+import { polyfillSticky } from '~/lib/utils/sticky';
+import bp from '~/breakpoints';
+import CiHeader from '~/vue_shared/components/header_ci_component.vue';
+import Callout from '~/vue_shared/components/callout.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import createStore from '../store';
+import EmptyState from './empty_state.vue';
+import EnvironmentsBlock from './environments_block.vue';
+import ErasedBlock from './erased_block.vue';
+import Log from './job_log.vue';
+import LogTopBar from './job_log_controllers.vue';
+import StuckBlock from './stuck_block.vue';
+import Sidebar from './sidebar.vue';
+import { sprintf } from '~/locale';
+import delayedJobMixin from '../mixins/delayed_job_mixin';
- export default {
- name: 'JobPageApp',
- store: createStore(),
- components: {
- CiHeader,
- Callout,
- EmptyState,
- EnvironmentsBlock,
- ErasedBlock,
- Log,
- LogTopBar,
- StuckBlock,
- Sidebar,
+export default {
+ name: 'JobPageApp',
+ store: createStore(),
+ components: {
+ CiHeader,
+ Callout,
+ EmptyState,
+ EnvironmentsBlock,
+ ErasedBlock,
+ Icon,
+ Log,
+ LogTopBar,
+ StuckBlock,
+ Sidebar,
+ GlLoadingIcon,
+ },
+ mixins: [delayedJobMixin],
+ props: {
+ runnerSettingsUrl: {
+ type: String,
+ required: false,
+ default: null,
},
- props: {
- runnerSettingsUrl: {
- type: String,
- required: false,
- default: null,
- },
- runnerHelpUrl: {
- type: String,
- required: false,
- default: null,
- },
- endpoint: {
- type: String,
- required: true,
- },
- terminalPath: {
- type: String,
- required: false,
- default: null,
- },
- pagePath: {
- type: String,
- required: true,
- },
- logState: {
- type: String,
- required: true,
- },
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: null,
},
- computed: {
- ...mapState([
- 'isLoading',
- 'job',
- 'isSidebarOpen',
- 'trace',
- 'isTraceComplete',
- 'traceSize',
- 'isTraceSizeVisible',
- 'isScrollBottomDisabled',
- 'isScrollTopDisabled',
- 'isScrolledToBottomBeforeReceivingTrace',
- 'hasError',
- ]),
- ...mapGetters([
- 'headerActions',
- 'headerTime',
- 'shouldRenderCalloutMessage',
- 'shouldRenderTriggeredLabel',
- 'hasEnvironment',
- 'hasTrace',
- 'emptyStateIllustration',
- 'isScrollingDown',
- 'emptyStateAction',
- 'hasRunnersForProject',
- ]),
-
- shouldRenderContent() {
- return !this.isLoading && !this.hasError;
- }
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ terminalPath: {
+ type: String,
+ required: false,
+ default: null,
},
- watch: {
- // Once the job log is loaded,
- // fetch the stages for the dropdown on the sidebar
- job(newVal, oldVal) {
- if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
- this.fetchStages();
- }
- },
+ pagePath: {
+ type: String,
+ required: true,
},
- created() {
- this.throttled = _.throttle(this.toggleScrollButtons, 100);
+ logState: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'isLoading',
+ 'job',
+ 'isSidebarOpen',
+ 'trace',
+ 'isTraceComplete',
+ 'traceSize',
+ 'isTraceSizeVisible',
+ 'isScrollBottomDisabled',
+ 'isScrollTopDisabled',
+ 'isScrolledToBottomBeforeReceivingTrace',
+ 'hasError',
+ ]),
+ ...mapGetters([
+ 'headerActions',
+ 'headerTime',
+ 'shouldRenderCalloutMessage',
+ 'shouldRenderTriggeredLabel',
+ 'hasEnvironment',
+ 'hasTrace',
+ 'emptyStateIllustration',
+ 'isScrollingDown',
+ 'emptyStateAction',
+ 'hasRunnersForProject',
+ ]),
- this.setJobEndpoint(this.endpoint);
- this.setTraceOptions({
- logState: this.logState,
- pagePath: this.pagePath,
- });
+ shouldRenderContent() {
+ return !this.isLoading && !this.hasError;
+ },
- this.fetchJob();
- this.fetchTrace();
+ emptyStateTitle() {
+ const { emptyStateIllustration, remainingTime } = this;
+ const { title } = emptyStateIllustration;
- window.addEventListener('resize', this.onResize);
- window.addEventListener('scroll', this.updateScroll);
- },
+ if (this.isDelayedJob) {
+ return sprintf(title, { remainingTime });
+ }
- mounted() {
- this.updateSidebar();
+ return title;
},
+ },
+ watch: {
+ // Once the job log is loaded,
+ // fetch the stages for the dropdown on the sidebar
+ job(newVal, oldVal) {
+ if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
+ this.fetchStages();
+ }
- destroyed() {
- window.removeEventListener('resize', this.onResize);
- window.removeEventListener('scroll', this.updateScroll);
+ if (newVal.archived) {
+ this.$nextTick(() => {
+ if (this.$refs.sticky) {
+ polyfillSticky(this.$refs.sticky);
+ }
+ });
+ }
},
+ },
+ created() {
+ this.throttled = _.throttle(this.toggleScrollButtons, 100);
+
+ this.setJobEndpoint(this.endpoint);
+ this.setTraceOptions({
+ logState: this.logState,
+ pagePath: this.pagePath,
+ });
- methods: {
- ...mapActions([
- 'setJobEndpoint',
- 'setTraceOptions',
- 'fetchJob',
- 'fetchStages',
- 'hideSidebar',
- 'showSidebar',
- 'toggleSidebar',
- 'fetchTrace',
- 'scrollBottom',
- 'scrollTop',
- 'toggleScrollButtons',
- 'toggleScrollAnimation',
- ]),
- onResize() {
- this.updateSidebar();
- this.updateScroll();
- },
- updateSidebar() {
- if (bp.getBreakpointSize() === 'xs') {
- this.hideSidebar();
- } else if (!this.isSidebarOpen) {
- this.showSidebar();
- }
- },
- updateScroll() {
- if (!isScrolledToBottom()) {
- this.toggleScrollAnimation(false);
- } else if (this.isScrollingDown) {
- this.toggleScrollAnimation(true);
- }
+ this.fetchJob();
+ this.fetchTrace();
- this.throttled();
- },
+ window.addEventListener('resize', this.onResize);
+ window.addEventListener('scroll', this.updateScroll);
+ },
+ mounted() {
+ this.updateSidebar();
+ },
+ destroyed() {
+ window.removeEventListener('resize', this.onResize);
+ window.removeEventListener('scroll', this.updateScroll);
+ },
+ methods: {
+ ...mapActions([
+ 'setJobEndpoint',
+ 'setTraceOptions',
+ 'fetchJob',
+ 'fetchStages',
+ 'hideSidebar',
+ 'showSidebar',
+ 'toggleSidebar',
+ 'fetchTrace',
+ 'scrollBottom',
+ 'scrollTop',
+ 'toggleScrollButtons',
+ 'toggleScrollAnimation',
+ ]),
+ onResize() {
+ this.updateSidebar();
+ this.updateScroll();
+ },
+ updateSidebar() {
+ if (bp.getBreakpointSize() === 'xs') {
+ this.hideSidebar();
+ } else if (!this.isSidebarOpen) {
+ this.showSidebar();
+ }
+ },
+ updateScroll() {
+ if (!isScrolledToBottom()) {
+ this.toggleScrollAnimation(false);
+ } else if (this.isScrollingDown) {
+ this.toggleScrollAnimation(true);
+ }
+
+ this.throttled();
},
- };
+ },
+};
</script>
<template>
<div>
@@ -216,14 +240,28 @@
:erased-at="job.erased_at"
/>
+ <div
+ v-if="job.archived"
+ ref="sticky"
+ class="js-archived-job prepend-top-default archived-sticky sticky-top"
+ >
+ <icon
+ name="lock"
+ class="align-text-bottom"
+ />
+
+ {{ __('This job is archived. Only the complete pipeline can be retried.') }}
+ </div>
<!--job log -->
<div
v-if="hasTrace"
- class="build-trace-container prepend-top-default">
+ class="build-trace-container"
+ >
<log-top-bar
:class="{
'sidebar-expanded': isSidebarOpen,
- 'sidebar-collapsed': !isSidebarOpen
+ 'sidebar-collapsed': !isSidebarOpen,
+ 'has-archived-block': job.archived
}"
:erase-path="job.erase_path"
:size="traceSize"
@@ -248,7 +286,7 @@
class="js-job-empty-state"
:illustration-path="emptyStateIllustration.image"
:illustration-size-class="emptyStateIllustration.size"
- :title="emptyStateIllustration.title"
+ :title="emptyStateTitle"
:content="emptyStateIllustration.content"
:action="emptyStateAction"
/>
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index 6486b25c8a7..3ddcfd11dca 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -1,16 +1,21 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
+import tooltip from '~/vue_shared/directives/tooltip';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
+import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import { sprintf } from '~/locale';
export default {
components: {
CiIcon,
Icon,
+ GlLink,
},
directives: {
tooltip,
},
+ mixins: [delayedJobMixin],
props: {
job: {
type: Object,
@@ -23,7 +28,14 @@ export default {
},
computed: {
tooltipText() {
- return `${this.job.name} - ${this.job.status.tooltip}`;
+ const { name, status } = this.job;
+ const text = `${name} - ${status.tooltip}`;
+
+ if (this.isDelayedJob) {
+ return sprintf(text, { remainingTime: this.remainingTime });
+ }
+
+ return text;
},
},
};
@@ -37,11 +49,10 @@ export default {
active: isActive
}"
>
- <a
+ <gl-link
v-tooltip
:href="job.status.details_path"
:title="tooltipText"
- data-container="body"
data-boundary="viewport"
class="js-job-link"
>
@@ -60,6 +71,6 @@ export default {
name="retry"
class="js-retry-icon"
/>
- </a>
+ </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue
index ffa6ada3e28..92e20e92d66 100644
--- a/app/assets/javascripts/jobs/components/job_log.vue
+++ b/app/assets/javascripts/jobs/components/job_log.vue
@@ -1,45 +1,45 @@
<script>
- import { mapState, mapActions } from 'vuex';
+import { mapState, mapActions } from 'vuex';
- export default {
- name: 'JobLog',
- props: {
- trace: {
- type: String,
- required: true,
- },
- isComplete: {
- type: Boolean,
- required: true,
- },
+export default {
+ name: 'JobLog',
+ props: {
+ trace: {
+ type: String,
+ required: true,
},
- computed: {
- ...mapState(['isScrolledToBottomBeforeReceivingTrace']),
+ isComplete: {
+ type: Boolean,
+ required: true,
},
- updated() {
- this.$nextTick(() => this.handleScrollDown());
+ },
+ computed: {
+ ...mapState(['isScrolledToBottomBeforeReceivingTrace']),
+ },
+ updated() {
+ this.$nextTick(() => this.handleScrollDown());
+ },
+ mounted() {
+ this.$nextTick(() => this.handleScrollDown());
+ },
+ methods: {
+ ...mapActions(['scrollBottom']),
+ /**
+ * The job log is sent in HTML, which means we need to use `v-html` to render it
+ * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
+ * in this case because it runs before `v-html` has finished running, since there's no
+ * Vue binding.
+ * In order to scroll the page down after `v-html` has finished, we need to use setTimeout
+ */
+ handleScrollDown() {
+ if (this.isScrolledToBottomBeforeReceivingTrace) {
+ setTimeout(() => {
+ this.scrollBottom();
+ }, 0);
+ }
},
- mounted() {
- this.$nextTick(() => this.handleScrollDown());
- },
- methods: {
- ...mapActions(['scrollBottom']),
- /**
- * The job log is sent in HTML, which means we need to use `v-html` to render it
- * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
- * in this case because it runs before `v-html` has finished running, since there's no
- * Vue binding.
- * In order to scroll the page down after `v-html` has finished, we need to use setTimeout
- */
- handleScrollDown() {
- if (this.isScrolledToBottomBeforeReceivingTrace) {
- setTimeout(() => {
- this.scrollBottom();
- }, 0);
- }
- },
- },
- };
+ },
+};
</script>
<template>
<pre class="js-build-trace build-trace qa-build-trace">
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 94ab1b16c84..8b506b124ec 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective, GlLink, GlButton } from '@gitlab-org/gitlab-ui';
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg';
@@ -9,9 +9,11 @@ import scrollDown from '../svg/scroll_down.svg';
export default {
components: {
Icon,
+ GlLink,
+ GlButton,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
scrollDown,
props: {
@@ -67,82 +69,76 @@ export default {
};
</script>
<template>
- <div class="top-bar affix">
+ <div class="top-bar">
<!-- truncate information -->
<div class="js-truncated-info truncated-info d-none d-sm-block float-left">
<template v-if="isTraceSizeVisible">
{{ jobLogSize }}
- <a
+ <gl-link
v-if="rawPath"
:href="rawPath"
class="js-raw-link raw-link"
>
{{ s__("Job|Complete Raw") }}
- </a>
+ </gl-link>
</template>
</div>
<!-- eo truncate information -->
<div class="controllers float-right">
<!-- links -->
- <a
+ <gl-link
v-if="rawPath"
- v-tooltip
+ v-gl-tooltip.body
:title="s__('Job|Show complete raw')"
:href="rawPath"
class="js-raw-link-controller controllers-buttons"
- data-container="body"
>
<icon name="doc-text" />
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="erasePath"
- v-tooltip
+ v-gl-tooltip.body
:title="s__('Job|Erase job log')"
:href="erasePath"
:data-confirm="__('Are you sure you want to erase this build?')"
class="js-erase-link controllers-buttons"
- data-container="body"
data-method="post"
>
<icon name="remove" />
- </a>
+ </gl-link>
<!-- eo links -->
<!-- scroll buttons -->
<div
- v-tooltip
+ v-gl-tooltip
:title="s__('Job|Scroll to top')"
class="controllers-buttons"
- data-container="body"
>
- <button
+ <gl-button
:disabled="isScrollTopDisabled"
type="button"
class="js-scroll-top btn-scroll btn-transparent btn-blank"
@click="handleScrollToTop"
>
- <icon name="scroll_up"/>
- </button>
+ <icon name="scroll_up" />
+ </gl-button>
</div>
<div
- v-tooltip
+ v-gl-tooltip
:title="s__('Job|Scroll to bottom')"
class="controllers-buttons"
- data-container="body"
>
- <button
+ <gl-button
:disabled="isScrollBottomDisabled"
- type="button"
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
:class="{ animate: isScrollingDown }"
@click="handleScrollToBottom"
v-html="$options.scrollDown"
- >
- </button>
+ />
</div>
<!-- eo scroll buttons -->
</div>
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 28a02230d89..f7b7b8f10f7 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -1,6 +1,7 @@
<script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
+import { GlLink, GlButton } from '@gitlab-org/gitlab-ui';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
@@ -21,6 +22,8 @@ export default {
TriggerBlock,
StagesDropdown,
JobsContainer,
+ GlLink,
+ GlButton,
},
mixins: [timeagoMixin],
props: {
@@ -115,7 +118,7 @@ export default {
<strong class="inline prepend-top-8">
{{ job.name }}
</strong>
- <a
+ <gl-link
v-if="job.retry_path"
:class="retryButtonClass"
:href="job.retry_path"
@@ -123,8 +126,8 @@ export default {
rel="nofollow"
>
{{ __('Retry') }}
- </a>
- <a
+ </gl-link>
+ <gl-link
v-if="job.terminal_path"
:href="job.terminal_path"
class="js-terminal-link pull-right btn btn-primary
@@ -133,8 +136,8 @@ export default {
>
{{ __('Debug') }}
<icon name="external-link" />
- </a>
- <button
+ </gl-link>
+ <gl-button
:aria-label="__('Toggle Sidebar')"
type="button"
class="btn btn-blank gutter-toggle
@@ -146,20 +149,20 @@ export default {
data-hidden="true"
class="fa fa-angle-double-right"
></i>
- </button>
+ </gl-button>
</div>
<div
v-if="job.retry_path || job.new_issue_path"
class="block retry-link"
>
- <a
+ <gl-link
v-if="job.new_issue_path"
:href="job.new_issue_path"
class="js-new-issue btn btn-success btn-inverted"
>
{{ __('New issue') }}
- </a>
- <a
+ </gl-link>
+ <gl-link
v-if="job.retry_path"
:href="job.retry_path"
class="js-retry-job btn btn-inverted-secondary"
@@ -167,7 +170,7 @@ export default {
rel="nofollow"
>
{{ __('Retry') }}
- </a>
+ </gl-link>
</div>
<div :class="{ block : renderBlock }">
<p
@@ -177,9 +180,9 @@ export default {
<span class="build-light-text">
{{ __('Merge Request:') }}
</span>
- <a :href="job.merge_request.path">
+ <gl-link :href="job.merge_request.path">
!{{ job.merge_request.iid }}
- </a>
+ </gl-link>
</p>
<detail-row
@@ -244,14 +247,14 @@ export default {
v-if="job.cancel_path"
class="btn-group prepend-top-5"
role="group">
- <a
+ <gl-link
:href="job.cancel_path"
class="js-cancel-job btn btn-sm btn-default"
data-method="post"
rel="nofollow"
>
{{ __('Cancel') }}
- </a>
+ </gl-link>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index aeafe98a70b..cfedb38e17a 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -1,6 +1,11 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
+
export default {
name: 'SidebarDetailRow',
+ components: {
+ GlLink,
+ },
props: {
title: {
type: String,
@@ -41,7 +46,7 @@ export default {
v-if="hasHelpURL"
class="help-button float-right"
>
- <a
+ <gl-link
:href="helpUrl"
target="_blank"
rel="noopener noreferrer nofollow"
@@ -50,7 +55,7 @@ export default {
class="fa fa-question-circle"
aria-hidden="true"
></i>
- </a>
+ </gl-link>
</span>
</p>
</template>
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index 1d5789b175a..ca4bf471363 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -1,8 +1,12 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
/**
* Renders Stuck Runners block for job's view.
*/
export default {
+ components: {
+ GlLink,
+ },
props: {
hasNoRunnersForProject: {
type: Boolean,
@@ -52,12 +56,12 @@ export default {
</p>
{{ __("Go to") }}
- <a
+ <gl-link
v-if="runnersPath"
:href="runnersPath"
class="js-runners-path"
>
{{ __("Runners page") }}
- </a>
+ </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 41de4a6e85a..1e62c05b4d1 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -1,5 +1,10 @@
<script>
+import { GlButton } from '@gitlab-org/gitlab-ui';
+
export default {
+ components: {
+ GlButton,
+ },
props: {
trigger: {
type: Object,
@@ -41,15 +46,14 @@ export default {
</p>
<p v-if="hasVariables">
- <button
+ <gl-button
v-if="!areVariablesVisible"
type="button"
class="btn btn-default group js-reveal-variables"
@click="revealVariables"
>
{{ __('Reveal Variables') }}
- </button>
-
+ </gl-button>
</p>
<dl
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index ccd096a1da5..a32e945627c 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -23,4 +23,3 @@ export default () => {
},
});
};
-
diff --git a/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js b/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js
new file mode 100644
index 00000000000..8c7fb785a61
--- /dev/null
+++ b/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js
@@ -0,0 +1,50 @@
+import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility';
+
+export default {
+ data() {
+ return {
+ remainingTime: formatTime(0),
+ remainingTimeIntervalId: null,
+ };
+ },
+
+ mounted() {
+ this.startRemainingTimeInterval();
+ },
+
+ beforeDestroy() {
+ if (this.remainingTimeIntervalId) {
+ clearInterval(this.remainingTimeIntervalId);
+ }
+ },
+
+ computed: {
+ isDelayedJob() {
+ return this.job && this.job.scheduled;
+ },
+ },
+
+ watch: {
+ isDelayedJob() {
+ this.startRemainingTimeInterval();
+ },
+ },
+
+ methods: {
+ startRemainingTimeInterval() {
+ if (this.remainingTimeIntervalId) {
+ clearInterval(this.remainingTimeIntervalId);
+ }
+
+ if (this.isDelayedJob) {
+ this.updateRemainingTime();
+ this.remainingTimeIntervalId = setInterval(() => this.updateRemainingTime(), 1000);
+ }
+ },
+
+ updateRemainingTime() {
+ const remainingMilliseconds = calculateRemainingMilliseconds(this.job.scheduled_at);
+ this.remainingTime = formatTime(remainingMilliseconds);
+ },
+ },
+};
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 4de01f8e532..d440b2c9ef1 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -35,16 +35,19 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* Used to check if it should render the job log or the empty state
* @returns {Boolean}
*/
-export const hasTrace = state => state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running');
+export const hasTrace = state =>
+ state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running');
export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {};
-export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {};
+export const emptyStateAction = state =>
+ (state.job && state.job.status && state.job.status.action) || {};
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
-export const hasRunnersForProject = state => state.job.runners.available && !state.job.runners.online;
+export const hasRunnersForProject = state =>
+ state.job.runners.available && !state.job.runners.online;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 3c38d998b6c..c0a76814102 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -25,16 +25,43 @@ export default class LabelsSelect {
}
$els.each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
+ var $block,
+ $colorPreview,
+ $dropdown,
+ $form,
+ $loading,
+ $selectbox,
+ $sidebarCollapsedValue,
+ $value,
+ abilityName,
+ defaultLabel,
+ enableLabelCreateButton,
+ issueURLSplit,
+ issueUpdateURL,
+ labelUrl,
+ namespacePath,
+ projectPath,
+ saveLabelData,
+ selectedLabel,
+ showAny,
+ showNo,
+ $sidebarLabelTooltip,
+ initialSelected,
+ $toggleText,
+ fieldName,
+ useId,
+ propertyName,
+ showMenuAbove,
+ $container,
+ $dropdownContainer;
$dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespacePath');
projectPath = $dropdown.data('projectPath');
- labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected');
- if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
+ if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) {
selectedLabel = selectedLabel.split(',');
}
showNo = $dropdown.data('showNo');
@@ -50,26 +77,37 @@ export default class LabelsSelect {
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
fieldName = $dropdown.data('fieldName');
- useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
+ useId = $dropdown.is(
+ '.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown',
+ );
propertyName = useId ? 'id' : 'title';
initialSelected = $selectbox
.find('input[name="' + $dropdown.data('fieldName') + '"]')
- .map(function () {
+ .map(function() {
return this.value;
- }).get();
+ })
+ .get();
const { handleClick } = options;
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
+ new CreateLabelDropdown(
+ $dropdown.closest('.dropdown').find('.dropdown-new-label'),
+ namespacePath,
+ projectPath,
+ );
}
saveLabelData = function() {
var data, selected;
- selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
- return this.value;
- }).get();
+ selected = $dropdown
+ .closest('.selectbox')
+ .find("input[name='" + fieldName + "']")
+ .map(function() {
+ return this.value;
+ })
+ .get();
if (_.isEqual(initialSelected, selected)) return;
initialSelected = selected;
@@ -82,7 +120,8 @@ export default class LabelsSelect {
}
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
- axios.put(issueUpdateURL, data)
+ axios
+ .put(issueUpdateURL, data)
.then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
@@ -96,8 +135,7 @@ export default class LabelsSelect {
issueUpdateURL,
});
labelCount = data.labels.length;
- }
- else {
+ } else {
template = '<span class="no-value">None</span>';
}
$value.removeAttr('style').html(template);
@@ -114,17 +152,14 @@ export default class LabelsSelect {
}
labelTooltipTitle = labelTitles.join(', ');
- }
- else {
+ } else {
labelTooltipTitle = __('Labels');
}
- $sidebarLabelTooltip
- .attr('title', labelTooltipTitle)
- .tooltip('_fixTitle');
+ $sidebarLabelTooltip.attr('title', labelTooltipTitle).tooltip('_fixTitle');
$('.has-tooltip', $value).tooltip({
- container: 'body'
+ container: 'body',
});
})
.catch(() => flash(__('Error saving label update.')));
@@ -132,34 +167,39 @@ export default class LabelsSelect {
$dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
- axios.get(labelUrl)
- .then((res) => {
- let data = _.chain(res.data).groupBy(function(label) {
- return label.title;
- }).map(function(label) {
- var color;
- color = _.map(label, function(dup) {
- return dup.color;
- });
- return {
- id: label[0].id,
- title: label[0].title,
- color: color,
- duplicate: color.length > 1
- };
- }).value();
+ labelUrl = $dropdown.attr('data-labels');
+ axios
+ .get(labelUrl)
+ .then(res => {
+ let data = _.chain(res.data)
+ .groupBy(function(label) {
+ return label.title;
+ })
+ .map(function(label) {
+ var color;
+ color = _.map(label, function(dup) {
+ return dup.color;
+ });
+ return {
+ id: label[0].id,
+ title: label[0].title,
+ color: color,
+ duplicate: color.length > 1,
+ };
+ })
+ .value();
if ($dropdown.hasClass('js-extra-options')) {
var extraData = [];
if (showNo) {
extraData.unshift({
id: 0,
- title: 'No Label'
+ title: 'No Label',
});
}
if (showAny) {
extraData.unshift({
isAny: true,
- title: 'Any Label'
+ title: 'Any Label',
});
}
if (extraData.length) {
@@ -176,11 +216,22 @@ export default class LabelsSelect {
.catch(() => flash(__('Error fetching labels.')));
},
renderRow: function(label, instance) {
- var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
+ var $a,
+ $li,
+ color,
+ colorEl,
+ indeterminate,
+ removesAll,
+ selectedClass,
+ spacing,
+ i,
+ marked,
+ dropdownName,
+ dropdownValue;
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
- removesAll = label.id <= 0 || (label.id == null);
+ removesAll = label.id <= 0 || label.id == null;
if ($dropdown.hasClass('js-filter-bulk-update')) {
indeterminate = $dropdown.data('indeterminate') || [];
marked = $dropdown.data('marked') || [];
@@ -200,9 +251,19 @@ export default class LabelsSelect {
} else {
if (this.id(label)) {
dropdownName = $dropdown.data('fieldName');
- dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
-
- if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ dropdownValue = this.id(label)
+ .toString()
+ .replace(/'/g, "\\'");
+
+ if (
+ $form.find(
+ "input[type='hidden'][name='" +
+ dropdownName +
+ "'][value='" +
+ dropdownValue +
+ "']",
+ ).length
+ ) {
selectedClass.push('is-active');
}
}
@@ -213,16 +274,14 @@ export default class LabelsSelect {
}
if (label.duplicate) {
color = DropdownUtils.duplicateLabelColor(label.color);
- }
- else {
+ } else {
if (label.color != null) {
[color] = label.color;
}
}
if (color) {
colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
- }
- else {
+ } else {
colorEl = '';
}
// We need to identify which items are actually labels
@@ -235,7 +294,7 @@ export default class LabelsSelect {
return $li.html($a).prop('outerHTML');
},
search: {
- fields: ['title']
+ fields: ['title'],
},
selectable: true,
filterable: true,
@@ -255,25 +314,21 @@ export default class LabelsSelect {
if (selected && selected.id === 0) {
this.selected = [];
return 'No Label';
- }
- else if (isSelected) {
+ } else if (isSelected) {
this.selected.push(title);
- }
- else if (!isSelected && title) {
+ } else if (!isSelected && title) {
var index = this.selected.indexOf(title);
this.selected.splice(index, 1);
}
if (selectedLabels.length === 1) {
return selectedLabels;
- }
- else if (selectedLabels.length) {
+ } else if (selectedLabels.length) {
return sprintf(__('%{firstLabel} +%{labelCount} more'), {
firstLabel: selectedLabels[0],
- labelCount: selectedLabels.length - 1
+ labelCount: selectedLabels.length - 1,
});
- }
- else {
+ } else {
return defaultLabel;
}
},
@@ -285,10 +340,9 @@ export default class LabelsSelect {
return label.id;
}
- if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
+ if ($dropdown.hasClass('js-filter-submit') && label.isAny == null) {
return label.title;
- }
- else {
+ } else {
return label.id;
}
},
@@ -310,13 +364,13 @@ export default class LabelsSelect {
}
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
+ selectedLabels = $dropdown
+ .closest('form')
+ .find("input:hidden[name='" + $dropdown.data('fieldName') + "']");
Issuable.filterResults($dropdown.closest('form'));
- }
- else if ($dropdown.hasClass('js-filter-submit')) {
+ } else if ($dropdown.hasClass('js-filter-submit')) {
$dropdown.closest('form').submit();
- }
- else {
+ } else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
}
@@ -325,7 +379,7 @@ export default class LabelsSelect {
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function (clickEvent) {
+ clicked: function(clickEvent) {
const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
@@ -339,7 +393,8 @@ export default class LabelsSelect {
isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
- $dropdown.parent()
+ $dropdown
+ .parent()
.find('.dropdown-clear-active')
.removeClass('is-active');
}
@@ -367,28 +422,26 @@ export default class LabelsSelect {
e.preventDefault();
return;
- }
- else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form'));
}
- }
- else if ($dropdown.hasClass('js-filter-submit')) {
+ } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
- }
- else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if ($el.hasClass('is-active')) {
- boardsStore.detail.issue.labels.push(new ListLabel({
- id: label.id,
- title: label.title,
- color: label.color[0],
- textColor: '#fff'
- }));
- }
- else {
+ boardsStore.detail.issue.labels.push(
+ new ListLabel({
+ id: label.id,
+ title: label.title,
+ color: label.color[0],
+ textColor: '#fff',
+ }),
+ );
+ } else {
var { labels } = boardsStore.detail.issue;
- labels = labels.filter(function (selectedLabel) {
+ labels = labels.filter(function(selectedLabel) {
return selectedLabel.id !== label.id;
});
boardsStore.detail.issue.labels = labels;
@@ -396,19 +449,16 @@ export default class LabelsSelect {
$loading.fadeIn();
- boardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ boardsStore.detail.issue
+ .update($dropdown.attr('data-issue-update'))
.then(fadeOutLoader)
.catch(fadeOutLoader);
- }
- else if (handleClick) {
+ } else if (handleClick) {
e.preventDefault();
handleClick(label);
- }
- else {
+ } else {
if ($dropdown.hasClass('js-multiselect')) {
-
- }
- else {
+ } else {
return saveLabelData();
}
}
@@ -436,15 +486,17 @@ export default class LabelsSelect {
// so best approach is to use traditional way of
// concatenation
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
- const tpl = _.template([
- '<% _.each(labels, function(label){ %>',
- '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
- '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
- '<%- label.title %>',
- '</span>',
- '</a>',
- '<% }); %>',
- ].join(''));
+ const tpl = _.template(
+ [
+ '<% _.each(labels, function(label){ %>',
+ '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
+ '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
+ '<%- label.title %>',
+ '</span>',
+ '</a>',
+ '<% }); %>',
+ ].join(''),
+ );
return tpl(tplData);
}
diff --git a/app/assets/javascripts/lib/utils/ace_utils.js b/app/assets/javascripts/lib/utils/ace_utils.js
index efc4b2a8d94..ee71ae0e61a 100644
--- a/app/assets/javascripts/lib/utils/ace_utils.js
+++ b/app/assets/javascripts/lib/utils/ace_utils.js
@@ -1,6 +1,6 @@
/* global ace */
export default function getModeByFileExtension(path) {
- const modelist = ace.require("ace/ext/modelist");
+ const modelist = ace.require('ace/ext/modelist');
return modelist.getModeForPath(path).mode;
-};
+}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 46740308f17..59007d5950e 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -14,7 +14,7 @@ window.timeago = timeago;
*
* @param {Boolean} abbreviated
*/
-const getMonthNames = abbreviated => {
+export const getMonthNames = abbreviated => {
if (abbreviated) {
return [
s__('Jan'),
@@ -454,12 +454,20 @@ export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {})
/**
* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
+ * If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days'
*/
-export const stringifyTime = timeObject => {
+export const stringifyTime = (timeObject, fullNameFormat = false) => {
const reducedTime = _.reduce(
timeObject,
(memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
+
+ if (fullNameFormat && isNonZero) {
+ // Remove traling 's' if unit value is singular
+ const formatedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
+ return `${memo} ${unitValue} ${formatedUnitName}`;
+ }
+
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
},
'',
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index afbab59055b..2ccc51c35f7 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -7,7 +7,7 @@ import { BYTES_IN_KIB } from './constants';
* * * Show 3 digits to the right
* * For 2 digits to the left of the decimal point and X digits to the right of it
* * * Show 2 digits to the right
-*/
+ */
export function formatRelevantDigits(number) {
let digitsLeft = '';
let relevantDigits = 0;
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index 7d0c701fd70..bd263c75a3d 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -7,8 +7,12 @@ export default class Members {
}
addListeners() {
- $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
- $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
+ $('.js-member-update-control')
+ .off('change')
+ .on('change', this.formSubmit.bind(this));
+ $('.js-edit-member-form')
+ .off('ajax:success')
+ .on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
@@ -28,7 +32,7 @@ export default class Members {
toggleLabel(selected, $el) {
return $el.text();
},
- clicked: (options) => {
+ clicked: options => {
this.formSubmit(null, options.$el);
},
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 2950c2299ab..d8255181574 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -11,7 +11,6 @@ import bp from './breakpoints';
import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { getLocationHash } from './lib/utils/url_utility';
-import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
@@ -207,8 +206,6 @@ export default class MergeRequestTabs {
}
this.resetViewContainer();
this.destroyPipelinesView();
-
- initDiscussionTab();
}
if (this.setUrl) {
this.setCurrentAction(action);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 42fb5c7177a..d32f39881dd 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -9,7 +9,10 @@ import '~/gl_dropdown';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
-import boardsStore, { boardStoreIssueSet, boardStoreIssueDelete } from './boards/stores/boards_store';
+import boardsStore, {
+ boardStoreIssueSet,
+ boardStoreIssueDelete,
+} from './boards/stores/boards_store';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index b980e43b898..754c6e79ee4 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -321,10 +321,10 @@ Please check your network connection and try again.`;
v-else-if="!canCreateNote"
:issuable-type="issuableTypeTitle"
/>
- <ul
+ <div
v-else-if="canCreateNote"
class="notes notes-form timeline">
- <li class="timeline-entry">
+ <div class="timeline-entry note-form">
<div class="timeline-entry-inner">
<div class="flash-container error-alert timeline-content"></div>
<div class="timeline-icon d-none d-sm-none d-md-block">
@@ -390,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled="isSubmitButtonDisabled"
name="button"
type="button"
- class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
+ class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
data-display="static"
data-toggle="dropdown"
aria-label="Open comment type dropdown">
@@ -422,7 +422,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
<button
type="button"
- class="btn btn-transparent"
+ class="btn btn-transparent qa-discussion-option"
@click.prevent="setNoteType('discussion')">
<i
aria-hidden="true"
@@ -462,7 +462,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</form>
</div>
</div>
- </li>
- </ul>
+ </div>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index eaa0cded224..b209f736c3f 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,15 +1,18 @@
<script>
import { mapState, mapActions } from 'vuex';
-import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
-import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
+import { trimFirstCharOfLineContent, getDiffMode } from '~/diffs/store/utils';
export default {
components: {
DiffFileHeader,
GlSkeletonLoading,
+ DiffViewer,
+ ImageDiffOverlay,
},
props: {
discussion: {
@@ -25,7 +28,11 @@ export default {
computed: {
...mapState({
noteableData: state => state.notes.noteableData,
+ projectPath: state => state.diffs.projectPath,
}),
+ diffMode() {
+ return getDiffMode(this.diffFile);
+ },
hasTruncatedDiffLines() {
return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
},
@@ -62,11 +69,7 @@ export default {
},
},
mounted() {
- if (this.isImageDiff) {
- const canCreateNote = false;
- const renderCommentBadge = true;
- imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
- } else if (!this.hasTruncatedDiffLines) {
+ if (!this.hasTruncatedDiffLines) {
this.fetchDiff();
}
},
@@ -160,7 +163,24 @@ export default {
<div
v-else
>
- <div v-html="imageDiffHtml"></div>
+ <diff-viewer
+ :diff-mode="diffMode"
+ :new-path="diffFile.newPath"
+ :new-sha="diffFile.diffRefs.headSha"
+ :old-path="diffFile.oldPath"
+ :old-sha="diffFile.diffRefs.baseSha"
+ :file-hash="diffFile.fileHash"
+ :project-path="projectPath"
+ >
+ <image-diff-overlay
+ slot="image-overlay"
+ :discussions="discussion"
+ :file-hash="diffFile.fileHash"
+ :show-comment-icon="true"
+ :should-toggle-discussion="false"
+ badge-class="image-comment-badge"
+ />
+ </diff-viewer>
<slot></slot>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 1f80f24e045..a4d76a70696 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,9 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import resolveSvg from 'icons/_icon_resolve_discussion.svg';
-import resolvedSvg from 'icons/_icon_status_success_solid.svg';
-import mrIssueSvg from 'icons/_icon_mr_issue.svg';
-import nextDiscussionSvg from 'icons/_next_discussion.svg';
+import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '../../lib/utils/text_utility';
import discussionNavigation from '../mixins/discussion_navigation';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -12,6 +9,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ Icon,
+ },
mixins: [discussionNavigation],
computed: {
...mapGetters([
@@ -37,12 +37,6 @@ export default {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
},
- created() {
- this.resolveSvg = resolveSvg;
- this.resolvedSvg = resolvedSvg;
- this.mrIssueSvg = mrIssueSvg;
- this.nextDiscussionSvg = nextDiscussionSvg;
- },
methods: {
...mapActions(['expandDiscussion']),
jumpToFirstUnresolvedDiscussion() {
@@ -66,15 +60,9 @@ export default {
<span
:class="{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled"
- type="button">
- <span
- v-if="allResolved"
- v-html="resolvedSvg"
- ></span>
- <span
- v-else
- v-html="resolveSvg"
- ></span>
+ type="button"
+ >
+ <icon name="check-circle" />
</span>
<span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
@@ -90,7 +78,7 @@ export default {
:title="s__('Resolve all discussions in new issue')"
data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
- <span v-html="mrIssueSvg"></span>
+ <icon name="issue-new" />
</a>
</div>
<div
@@ -103,7 +91,7 @@ export default {
data-container="body"
class="btn btn-default discussion-next-btn"
@click="jumpToFirstUnresolvedDiscussion">
- <span v-html="nextDiscussionSvg"></span>
+ <icon name="comment-next" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 6e8f43048d1..affa2d1b574 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -1,7 +1,8 @@
<script>
import $ from 'jquery';
-import Icon from '~/vue_shared/components/icon.vue';
import { mapGetters, mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE } from '../constants';
export default {
components: {
@@ -12,14 +13,17 @@ export default {
type: Array,
required: true,
},
- defaultValue: {
+ selectedValue: {
type: Number,
default: null,
required: false,
},
},
data() {
- return { currentValue: this.defaultValue };
+ return {
+ currentValue: this.selectedValue,
+ defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
+ };
},
computed: {
...mapGetters(['getNotesDataByProp']),
@@ -28,8 +32,11 @@ export default {
return this.filters.find(filter => filter.value === this.currentValue);
},
},
+ mounted() {
+ this.toggleCommentsForm();
+ },
methods: {
- ...mapActions(['filterDiscussion']),
+ ...mapActions(['filterDiscussion', 'setCommentsDisabled']),
selectFilter(value) {
const filter = parseInt(value, 10);
@@ -39,6 +46,10 @@ export default {
if (filter === this.currentValue) return;
this.currentValue = filter;
this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter });
+ this.toggleCommentsForm();
+ },
+ toggleCommentsForm() {
+ this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE);
},
},
};
@@ -73,6 +84,10 @@ export default {
>
{{ filter.title }}
</button>
+ <div
+ v-if="filter.value === defaultValue"
+ class="dropdown-divider"
+ ></div>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index e075f94b82b..f7a61fbfcd4 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,19 +1,14 @@
<script>
import { mapGetters } from 'vuex';
-import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
-import emojiSmile from 'icons/_emoji_smile.svg';
-import emojiSmiley from 'icons/_emoji_smiley.svg';
-import editSvg from 'icons/_icon_pencil.svg';
-import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
-import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
-import ellipsisSvg from 'icons/_ellipsis_v.svg';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
name: 'NoteActions',
components: {
Icon,
+ GlLoadingIcon,
},
directives: {
tooltip,
@@ -108,15 +103,6 @@ export default {
return title;
},
},
- created() {
- this.emojiSmiling = emojiSmiling;
- this.emojiSmile = emojiSmile;
- this.emojiSmiley = emojiSmiley;
- this.editSvg = editSvg;
- this.ellipsisSvg = ellipsisSvg;
- this.resolveDiscussionSvg = resolveDiscussionSvg;
- this.resolvedDiscussionSvg = resolvedDiscussionSvg;
- },
methods: {
onEdit() {
this.$emit('handleEdit');
@@ -150,12 +136,7 @@ export default {
class="line-resolve-btn note-action-button"
@click="onResolve">
<template v-if="!isResolving">
- <div
- v-if="isResolved"
- v-html="resolvedDiscussionSvg"></div>
- <div
- v-else
- v-html="resolveDiscussionSvg"></div>
+ <icon name="check-circle" />
</template>
<gl-loading-icon
v-else
@@ -177,18 +158,18 @@ export default {
title="Add reaction"
>
<gl-loading-icon inline/>
- <span
- class="link-highlight award-control-icon-neutral"
- v-html="emojiSmiling">
- </span>
- <span
- class="link-highlight award-control-icon-positive"
- v-html="emojiSmiley">
- </span>
- <span
- class="link-highlight award-control-icon-super-positive"
- v-html="emojiSmile">
- </span>
+ <icon
+ css-classes="link-highlight award-control-icon-neutral"
+ name="emoji_slightly_smiling_face"
+ />
+ <icon
+ css-classes="link-highlight award-control-icon-positive"
+ name="emoji_smiley"
+ />
+ <icon
+ css-classes="link-highlight award-control-icon-super-positive"
+ name="emoji_smiley"
+ />
</a>
</div>
<div
@@ -202,10 +183,10 @@ export default {
data-container="body"
data-placement="bottom"
@click="onEdit">
- <span
- class="link-highlight"
- v-html="editSvg">
- </span>
+ <icon
+ name="pencil"
+ css-classes="link-highlight"
+ />
</button>
</div>
<div
@@ -238,15 +219,15 @@ export default {
data-toggle="dropdown"
data-container="body"
data-placement="bottom">
- <span
- class="icon"
- v-html="ellipsisSvg">
- </span>
+ <icon
+ css-classes="icon"
+ name="ellipsis_v"
+ />
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse">
<a :href="reportAbusePath">
- Report as abuse
+ {{ __('Report abuse to GitLab') }}
</a>
</li>
<li v-if="noteUrl">
@@ -255,7 +236,7 @@ export default {
type="button"
class="btn-default btn-transparent js-btn-copy-note-link"
>
- Copy link
+ {{ __('Copy link') }}
</button>
</li>
<li v-if="canEdit">
@@ -264,7 +245,7 @@ export default {
type="button"
@click.prevent="onDelete">
<span class="text-danger">
- Delete comment
+ {{ __('Delete comment') }}
</span>
</button>
</li>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index e707f44bf5a..401bcfabbe4 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,13 +1,14 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
-import emojiSmile from 'icons/_emoji_smile.svg';
-import emojiSmiley from 'icons/_emoji_smiley.svg';
+import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
@@ -72,11 +73,6 @@ export default {
return this.noteAuthorId === this.getUserData.id;
},
},
- created() {
- this.emojiSmiling = emojiSmiling;
- this.emojiSmile = emojiSmile;
- this.emojiSmiley = emojiSmiley;
- },
methods: {
...mapActions(['toggleAwardRequest']),
getAwardHTML(name) {
@@ -110,7 +106,7 @@ export default {
// Get the remaining list to use in `and x more` text.
const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length);
- // Add myself to the begining of the list so title will start with You.
+ // Add myself to the beginning of the list so title will start with You.
if (hasReactionByCurrentUser) {
namesToShow.unshift('You');
}
@@ -196,17 +192,14 @@ export default {
data-boundary="viewport"
data-placement="bottom"
type="button">
- <span
- class="award-control-icon award-control-icon-neutral"
- v-html="emojiSmiling">
+ <span class="award-control-icon award-control-icon-neutral">
+ <icon name="emoji_slightly_smiling_face" />
</span>
- <span
- class="award-control-icon award-control-icon-positive"
- v-html="emojiSmiley">
+ <span class="award-control-icon award-control-icon-positive">
+ <icon name="emoji_smiley" />
</span>
- <span
- class="award-control-icon award-control-icon-super-positive"
- v-html="emojiSmile">
+ <span class="award-control-icon award-control-icon-super-positive">
+ <icon name="emoji_smiley" />
</span>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 38c43e5fe08..31ee8fed984 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -187,7 +187,7 @@ export default {
:data-supports-quick-actions="!isEditing"
name="note[note]"
class="note-textarea js-gfm-input js-note-text
-js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
+js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleUpdate()"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 7b6e7b72caf..dd7313d7b10 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -45,6 +45,9 @@ export default {
noteTimestampLink() {
return `#note_${this.noteId}`;
},
+ hasAuthor() {
+ return this.author && Object.keys(this.author).length;
+ },
},
methods: {
...mapActions(['setTargetNoteHash']),
@@ -76,7 +79,7 @@ export default {
</button>
</div>
<a
- v-if="Object.keys(author).length"
+ v-if="hasAuthor"
:href="author.path"
>
<span class="note-header-author-name">{{ author.name }}</span>
@@ -92,9 +95,6 @@ export default {
</span>
<span class="note-headline-light">
<span class="note-headline-meta">
- <template v-if="actionText">
- {{ actionText }}
- </template>
<span class="system-note-message">
<slot></slot>
</span>
@@ -102,7 +102,9 @@ export default {
v-if="createdAt"
>
<span class="system-note-separator">
- &middot;
+ <template v-if="actionText">
+ {{ actionText }}
+ </template>
</span>
<a
:href="noteTimestampLink"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index c5fdfa1d47c..c1dfa036678 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,16 +1,16 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
-import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
-import systemNote from '~/vue_shared/components/notes/system_note.vue';
import { s__ } from '~/locale';
+import systemNote from '~/vue_shared/components/notes/system_note.vue';
+import icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
+import toggleRepliesWidget from './toggle_replies_widget.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
@@ -26,6 +26,7 @@ import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'NoteableDiscussion',
components: {
+ icon,
noteableNote,
diffWithNote,
userAvatarLink,
@@ -33,6 +34,7 @@ export default {
noteSignedOutWidget,
noteEditedText,
noteForm,
+ toggleRepliesWidget,
placeholderNote,
placeholderSystemNote,
systemNote,
@@ -46,11 +48,6 @@ export default {
type: Object,
required: true,
},
- renderHeader: {
- type: Boolean,
- required: false,
- default: true,
- },
renderDiffFile: {
type: Boolean,
required: false,
@@ -72,6 +69,7 @@ export default {
isReplying: false,
isResolving: false,
resolveAsThread: true,
+ isRepliesCollapsed: (!this.discussion.diff_discussion && this.discussion.resolved) || false,
};
},
computed: {
@@ -112,6 +110,15 @@ export default {
newNotePath() {
return this.getNoteableData.create_note_path;
},
+ hasReplies() {
+ return this.discussion.notes.length > 1;
+ },
+ initialDiscussion() {
+ return this.discussion.notes.slice(0, 1)[0];
+ },
+ replies() {
+ return this.discussion.notes.slice(1);
+ },
lastUpdatedBy() {
const { notes } = this.discussion;
@@ -147,6 +154,12 @@ export default {
return diffDiscussion && diffFile && this.renderDiffFile;
},
+ shouldGroupReplies() {
+ return !this.shouldRenderDiffs && !this.transformedDiscussion.diffDiscussion;
+ },
+ shouldRenderHeader() {
+ return this.shouldRenderDiffs;
+ },
wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div';
},
@@ -160,6 +173,22 @@ export default {
wrapperClass() {
return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
},
+ componentClassName() {
+ if (this.shouldRenderDiffs) {
+ if (!this.lastUpdatedAt && !this.discussion.resolved) {
+ return 'unresolved';
+ }
+ }
+
+ return '';
+ },
+ shouldShowDiscussions() {
+ const isExpanded = this.discussion.expanded;
+ const { diffDiscussion, resolved } = this.transformedDiscussion;
+ const isResolvedNonDiffDiscussion = !diffDiscussion && resolved;
+
+ return isExpanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
+ },
},
watch: {
isReplying() {
@@ -173,10 +202,6 @@ export default {
}
},
},
- created() {
- this.resolveDiscussionsSvg = resolveDiscussionsSvg;
- this.nextDiscussionsSvg = nextDiscussionsSvg;
- },
methods: {
...mapActions([
'saveNote',
@@ -207,6 +232,9 @@ export default {
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id });
},
+ toggleReplies() {
+ this.isRepliesCollapsed = !this.isRepliesCollapsed;
+ },
showReplyForm() {
this.isReplying = true;
},
@@ -274,26 +302,29 @@ Please check your network connection and try again.`;
</script>
<template>
- <li class="note note-discussion timeline-entry">
+ <li
+ class="note note-discussion timeline-entry"
+ :class="componentClassName"
+ >
<div class="timeline-entry-inner">
- <div class="timeline-icon">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
<div class="timeline-content">
<div
:data-discussion-id="transformedDiscussion.discussion_id"
class="discussion js-discussion-container"
>
<div
- v-if="renderHeader"
- class="discussion-header"
+ v-if="shouldRenderHeader"
+ class="discussion-header note-wrapper"
>
+ <div class="timeline-icon">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ />
+ </div>
<note-header
:author="author"
:created-at="transformedDiscussion.created_at"
@@ -339,7 +370,7 @@ Please check your network connection and try again.`;
/>
</div>
<div
- v-if="discussion.expanded || alwaysExpanded"
+ v-if="shouldShowDiscussions"
class="discussion-body">
<component
:is="wrapperComponent"
@@ -348,38 +379,70 @@ Please check your network connection and try again.`;
>
<div class="discussion-notes">
<ul class="notes">
- <component
- :is="componentName(note)"
- v-for="note in discussion.notes"
- :key="note.id"
- :note="componentData(note)"
- @handleDeleteNote="deleteNoteHandler"
- />
+ <template v-if="shouldGroupReplies">
+ <component
+ :is="componentName(initialDiscussion)"
+ :note="componentData(initialDiscussion)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot
+ slot="avatar-badge"
+ name="avatar-badge"
+ >
+ </slot>
+ </component>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="isRepliesCollapsed"
+ :replies="replies"
+ @toggle="toggleReplies"
+ />
+ <template v-if="!isRepliesCollapsed">
+ <component
+ :is="componentName(note)"
+ v-for="note in replies"
+ :key="note.id"
+ :note="componentData(note)"
+ @handleDeleteNote="deleteNoteHandler"
+ />
+ </template>
+ </template>
+ <template v-else>
+ <component
+ :is="componentName(note)"
+ v-for="(note, index) in discussion.notes"
+ :key="note.id"
+ :note="componentData(note)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot
+ v-if="index === 0"
+ slot="avatar-badge"
+ name="avatar-badge"
+ >
+ </slot>
+ </component>
+ </template>
</ul>
<div
+ v-if="!isRepliesCollapsed"
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder"
>
<template v-if="!isReplying && canReply">
- <div
- class="btn-group d-flex discussion-with-resolve-btn"
- role="group">
- <div
- class="btn-group w-100"
- role="group">
- <button
- type="button"
- class="js-vue-discussion-reply btn btn-text-field mr-2"
- title="Add a reply"
- @click="showReplyForm">Reply...</button>
- </div>
- <div
- v-if="discussion.resolvable"
- class="btn-group"
- role="group">
+ <div class="discussion-with-resolve-btn">
+ <button
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply"
+ title="Add a reply"
+ @click="showReplyForm"
+ >
+ Reply...
+ </button>
+ <div v-if="discussion.resolvable">
<button
type="button"
- class="btn btn-default"
+ class="btn btn-default mr-sm-2"
@click="resolveHandler()"
>
<i
@@ -392,7 +455,7 @@ Please check your network connection and try again.`;
</div>
<div
v-if="discussion.resolvable"
- class="btn-group discussion-actions"
+ class="btn-group discussion-actions ml-sm-2"
role="group"
>
<div
@@ -407,7 +470,7 @@ Please check your network connection and try again.`;
btn-default discussion-create-issue-btn"
data-container="body"
>
- <span v-html="resolveDiscussionsSvg"></span>
+ <icon name="issue-new" />
</a>
</div>
<div
@@ -421,7 +484,7 @@ Please check your network connection and try again.`;
data-container="body"
@click="jumpToNextDiscussion"
>
- <span v-html="nextDiscussionsSvg"></span>
+ <icon name="comment-next" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index f391ed848a4..e302a89ee95 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -173,7 +173,7 @@ export default {
:class="classNameBindings"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note timeline-entry"
+ class="note timeline-entry note-wrapper"
>
<div class="timeline-entry-inner">
<div class="timeline-icon">
@@ -182,7 +182,13 @@ export default {
:img-src="author.avatar_url"
:img-alt="author.name"
:img-size="40"
- />
+ >
+ <slot
+ slot="avatar-badge"
+ name="avatar-badge"
+ >
+ </slot>
+ </user-avatar-link>
</div>
<div class="timeline-content">
<div class="note-header">
@@ -190,6 +196,7 @@ export default {
:author="author"
:created-at="note.created_at"
:note-id="note.id"
+ action-text="commented"
/>
<note-actions
:author-id="author.id"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index b0faa443a18..e555279a6ac 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -50,11 +50,19 @@ export default {
},
data() {
return {
+ isFetching: false,
currentFilter: null,
};
},
computed: {
- ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']),
+ ...mapGetters([
+ 'isNotesFetched',
+ 'discussions',
+ 'getNotesDataByProp',
+ 'discussionCount',
+ 'isLoading',
+ 'commentsDisabled',
+ ]),
noteableType() {
return this.noteableData.noteableType;
},
@@ -134,6 +142,10 @@ export default {
return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
},
fetchNotes() {
+ if (this.isFetching) return null;
+
+ this.isFetching = true;
+
return this.fetchDiscussions({ path: this.getNotesDataByProp('discussionsPath') })
.then(() => {
this.initPolling();
@@ -142,6 +154,7 @@ export default {
this.setLoadingState(false);
this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
+ this.isFetching = false;
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())
@@ -200,6 +213,7 @@ export default {
</ul>
<comment-form
+ v-if="!commentsDisabled"
:noteable-type="noteableType"
:markdown-version="markdownVersion"
/>
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
new file mode 100644
index 00000000000..78ecbbb9247
--- /dev/null
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -0,0 +1,94 @@
+<script>
+import _ from 'underscore';
+import Icon from '~/vue_shared/components/icon.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ Icon,
+ UserAvatarLink,
+ TimeAgoTooltip,
+ },
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ replies: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ lastReply() {
+ return this.replies[this.replies.length - 1];
+ },
+ uniqueAuthors() {
+ const authors = this.replies.map(reply => reply.author || {});
+
+ return _.uniq(authors, author => author.username);
+ },
+ className() {
+ return this.collapsed ? 'collapsed' : 'expanded';
+ },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ :class="className"
+ class="replies-toggle"
+ >
+ <template v-if="collapsed">
+ <icon
+ name="chevron-right"
+ @click.native="toggle"
+ />
+ <div>
+ <user-avatar-link
+ v-for="author in uniqueAuthors"
+ :key="author.username"
+ :link-href="author.path"
+ :img-alt="author.name"
+ :img-src="author.avatar_url"
+ :img-size="26"
+ :tooltip-text="author.name"
+ tooltip-placement="bottom"
+ />
+ </div>
+ <button
+ class="btn btn-link js-replies-text"
+ type="button"
+ @click="toggle"
+ >
+ {{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
+ </button>
+ {{ __('Last reply by') }}
+ <a
+ :href="lastReply.author.path"
+ class="btn btn-link author-link"
+ >
+ {{ lastReply.author.name }}
+ </a>
+ <time-ago-tooltip
+ :time="lastReply.created_at"
+ tooltip-placement="bottom"
+ />
+ </template>
+ <span
+ v-else
+ class="collapse-replies-btn js-collapse-replies"
+ @click="toggle"
+ >
+ <icon name="chevron-down" />
+ {{ s__('Notes|Collapse replies') }}
+ </span>
+ </li>
+</template>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index 2c3e07c0506..3147dc64c27 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -15,6 +15,8 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description';
+export const HISTORY_ONLY_FILTER_VALUE = 2;
+export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0;
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js
index 012ffc4093e..5c5f38a3fb0 100644
--- a/app/assets/javascripts/notes/discussion_filters.js
+++ b/app/assets/javascripts/notes/discussion_filters.js
@@ -1,15 +1,17 @@
import Vue from 'vue';
import DiscussionFilter from './components/discussion_filter.vue';
-export default (store) => {
+export default store => {
const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
if (discussionFilterEl) {
const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
- const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
+ const selectedValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
- const filters = Object.keys(filterValues).map(entry =>
- ({ title: entry, value: filterValues[entry] }));
+ const filters = Object.keys(filterValues).map(entry => ({
+ title: entry,
+ value: filterValues[entry],
+ }));
return new Vue({
el: discussionFilterEl,
@@ -22,7 +24,7 @@ export default (store) => {
return createElement('discussion-filter', {
props: {
filters,
- defaultValue,
+ selectedValue,
},
});
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index b5dd49bc6c9..88739ffb083 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -364,5 +364,9 @@ export const filterDiscussion = ({ dispatch }, { path, filter }) => {
});
};
+export const setCommentsDisabled = ({ commit }, data) => {
+ commit(types.DISABLE_COMMENTS, data);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js
index 4532226aa07..bee6d4f0329 100644
--- a/app/assets/javascripts/notes/stores/collapse_utils.js
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -70,7 +70,7 @@ export const collapseSystemNotes = notes => {
} else if (lastDescriptionSystemNote) {
const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
- // are they less than 10 minutes appart?
+ // are they less than 10 minutes apart?
if (timeDifferenceMinutes > 10) {
// reset counter
descriptionChangedTimes = 1;
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index e4f36154fcd..8df95c279eb 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -192,5 +192,7 @@ export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
return getters.unresolvedDiscussionsIdsByDate[0];
};
+export const commentsDisabled = state => state.commentsDisabled;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 400142668ea..8aea269ea7d 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -21,6 +21,7 @@ export default () => ({
noteableData: {
current_user: {},
},
+ commentsDisabled: false,
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 2fa53aef1d4..dfbf3b7b34b 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -15,6 +15,7 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
+export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 65085452139..c8d9e196103 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -225,4 +225,8 @@ export default {
discussion.truncated_diff_lines = diffLines;
},
+
+ [types.DISABLE_COMMENTS](state, value) {
+ state.commentsDisabled = value;
+ },
};
diff --git a/app/assets/javascripts/pages/groups/clusters/destroy/index.js b/app/assets/javascripts/pages/groups/clusters/destroy/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/destroy/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/index/index.js b/app/assets/javascripts/pages/groups/clusters/index/index.js
new file mode 100644
index 00000000000..845a5f7042c
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/index/index.js
@@ -0,0 +1,5 @@
+import initDismissableCallout from '~/dismissable_callout';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initDismissableCallout('.gcp-signup-offer');
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/show/index.js b/app/assets/javascripts/pages/groups/clusters/show/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/show/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/update/index.js b/app/assets/javascripts/pages/groups/clusters/update/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/update/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/index.js b/app/assets/javascripts/pages/groups/index.js
new file mode 100644
index 00000000000..bf80d8b8193
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/index.js
@@ -0,0 +1,16 @@
+import initDismissableCallout from '~/dismissable_callout';
+import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const { page } = document.body.dataset;
+ const newClusterViews = [
+ 'groups:clusters:new',
+ 'groups:clusters:create_gcp',
+ 'groups:clusters:create_user',
+ ];
+
+ if (newClusterViews.indexOf(page) > -1) {
+ initDismissableCallout('.gcp-signup-offer');
+ initGkeDropdowns();
+ }
+});
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 2c683a39f42..9d19e4a095d 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -1,54 +1,66 @@
<script>
- import axios from '~/lib/utils/axios_utils';
- import createFlash from '~/flash';
- import GlModal from '~/vue_shared/components/gl_modal.vue';
- import { s__, sprintf } from '~/locale';
- import { visitUrl } from '~/lib/utils/url_utility';
- import eventHub from '../event_hub';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+import { visitUrl } from '~/lib/utils/url_utility';
+import eventHub from '../event_hub';
- export default {
- components: {
- GlModal,
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ milestoneTitle: {
+ type: String,
+ required: true,
},
- props: {
- milestoneTitle: {
- type: String,
- required: true,
- },
- url: {
- type: String,
- required: true,
- },
- groupName: {
- type: String,
- required: true,
- },
+ url: {
+ type: String,
+ required: true,
},
- computed: {
- title() {
- return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
- },
- text() {
- return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
+ groupName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), {
+ milestoneTitle: this.milestoneTitle,
+ });
+ },
+ text() {
+ return sprintf(
+ s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
Existing project milestones with the same title will be merged.
- This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName });
- },
+ This action cannot be reversed.`),
+ { milestoneTitle: this.milestoneTitle, groupName: this.groupName },
+ );
},
- methods: {
- onSubmit() {
- eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
- return axios.post(this.url, { params: { format: 'json' } })
- .then((response) => {
- eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true });
- visitUrl(response.data.url);
- })
- .catch((error) => {
- eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false });
- createFlash(error);
+ },
+ methods: {
+ onSubmit() {
+ eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
+ return axios
+ .post(this.url, { params: { format: 'json' } })
+ .then(response => {
+ eventHub.$emit('promoteMilestoneModal.requestFinished', {
+ milestoneUrl: this.url,
+ successful: true,
});
- },
+ visitUrl(response.data.url);
+ })
+ .catch(error => {
+ eventHub.$emit('promoteMilestoneModal.requestFinished', {
+ milestoneUrl: this.url,
+ successful: false,
+ });
+ createFlash(error);
+ });
},
- };
+ },
+};
</script>
<template>
<gl-modal
@@ -65,4 +77,3 @@
{{ text }}
</gl-modal>
</template>
-
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
deleted file mode 100644
index d4f34e32a48..00000000000
--- a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
-
-document.addEventListener('DOMContentLoaded', () => {
- initGkeDropdowns();
-});
diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js
index e4b8baede58..845a5f7042c 100644
--- a/app/assets/javascripts/pages/projects/clusters/index/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index/index.js
@@ -1,5 +1,5 @@
-import ClustersIndex from '~/clusters/clusters_index';
+import initDismissableCallout from '~/dismissable_callout';
document.addEventListener('DOMContentLoaded', () => {
- new ClustersIndex(); // eslint-disable-line no-new
+ initDismissableCallout('.gcp-signup-offer');
});
diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js
new file mode 100644
index 00000000000..1b57c67f16b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/jobs/index/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
+ remainingTimeElements.forEach(
+ el =>
+ new Vue({
+ ...GlCountdown,
+ el,
+ propsData: {
+ endDateString: el.dateTime,
+ },
+ }),
+ );
+});
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 52d66beefc9..a6bee49a6b1 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -64,7 +64,9 @@ export default class Project {
const projectId = $(this).data('project-id');
const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`;
Cookies.set(cookieKey, 'false');
- $(this).parents('.auto-devops-implicitly-enabled-banner').remove();
+ $(this)
+ .parents('.auto-devops-implicitly-enabled-banner')
+ .remove();
return e.preventDefault();
});
Project.projectSelectDropdown();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index a16f7e6b77c..c0ec7a5dc94 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -1,202 +1,200 @@
<script>
- import projectFeatureSetting from './project_feature_setting.vue';
- import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
- import projectSettingRow from './project_setting_row.vue';
- import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
- import { toggleHiddenClassBySelector } from '../external';
+import projectFeatureSetting from './project_feature_setting.vue';
+import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
+import projectSettingRow from './project_setting_row.vue';
+import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
+import { toggleHiddenClassBySelector } from '../external';
- export default {
- components: {
- projectFeatureSetting,
- projectFeatureToggle,
- projectSettingRow,
- },
+export default {
+ components: {
+ projectFeatureSetting,
+ projectFeatureToggle,
+ projectSettingRow,
+ },
- props: {
- currentSettings: {
- type: Object,
- required: true,
- },
- canChangeVisibilityLevel: {
- type: Boolean,
- required: false,
- default: false,
- },
- allowedVisibilityOptions: {
- type: Array,
- required: false,
- default: () => [0, 10, 20],
- },
- lfsAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- registryAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- visibilityHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- lfsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- registryHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- pagesAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- pagesAccessControlEnabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- pagesHelpPath: {
- type: String,
- required: false,
- default: '',
- },
+ props: {
+ currentSettings: {
+ type: Object,
+ required: true,
+ },
+ canChangeVisibilityLevel: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ allowedVisibilityOptions: {
+ type: Array,
+ required: false,
+ default: () => [0, 10, 20],
+ },
+ lfsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ registryAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ visibilityHelpPath: {
+ type: String,
+ required: false,
+ default: '',
},
+ lfsHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ registryHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ pagesAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pagesAccessControlEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pagesHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
- data() {
- const defaults = {
- visibilityOptions,
- visibilityLevel: visibilityOptions.PUBLIC,
- issuesAccessLevel: 20,
- repositoryAccessLevel: 20,
- mergeRequestsAccessLevel: 20,
- buildsAccessLevel: 20,
- wikiAccessLevel: 20,
- snippetsAccessLevel: 20,
- pagesAccessLevel: 20,
- containerRegistryEnabled: true,
- lfsEnabled: true,
- requestAccessEnabled: true,
- highlightChangesClass: false,
- };
+ data() {
+ const defaults = {
+ visibilityOptions,
+ visibilityLevel: visibilityOptions.PUBLIC,
+ issuesAccessLevel: 20,
+ repositoryAccessLevel: 20,
+ mergeRequestsAccessLevel: 20,
+ buildsAccessLevel: 20,
+ wikiAccessLevel: 20,
+ snippetsAccessLevel: 20,
+ pagesAccessLevel: 20,
+ containerRegistryEnabled: true,
+ lfsEnabled: true,
+ requestAccessEnabled: true,
+ highlightChangesClass: false,
+ };
- return { ...defaults, ...this.currentSettings };
- },
+ return { ...defaults, ...this.currentSettings };
+ },
- computed: {
- featureAccessLevelOptions() {
- const options = [
- [10, 'Only Project Members'],
- ];
- if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
- options.push([20, 'Everyone With Access']);
- }
- return options;
- },
+ computed: {
+ featureAccessLevelOptions() {
+ const options = [[10, 'Only Project Members']];
+ if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
+ options.push([20, 'Everyone With Access']);
+ }
+ return options;
+ },
- repoFeatureAccessLevelOptions() {
- return this.featureAccessLevelOptions.filter(
- ([value]) => value <= this.repositoryAccessLevel,
- );
- },
+ repoFeatureAccessLevelOptions() {
+ return this.featureAccessLevelOptions.filter(
+ ([value]) => value <= this.repositoryAccessLevel,
+ );
+ },
- pagesFeatureAccessLevelOptions() {
- if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
- return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
- }
- return this.featureAccessLevelOptions;
- },
+ pagesFeatureAccessLevelOptions() {
+ if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
+ return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
+ }
+ return this.featureAccessLevelOptions;
+ },
- repositoryEnabled() {
- return this.repositoryAccessLevel > 0;
- },
+ repositoryEnabled() {
+ return this.repositoryAccessLevel > 0;
+ },
- visibilityLevelDescription() {
- return visibilityLevelDescriptions[this.visibilityLevel];
- },
+ visibilityLevelDescription() {
+ return visibilityLevelDescriptions[this.visibilityLevel];
},
+ },
- watch: {
- visibilityLevel(value, oldValue) {
- if (value === visibilityOptions.PRIVATE) {
- // when private, features are restricted to "only team members"
- this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
- this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
- this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
- this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
- this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
- this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
- if (this.pagesAccessLevel === 20) {
- // When from Internal->Private narrow access for only members
- this.pagesAccessLevel = 10;
- }
- this.highlightChanges();
- } else if (oldValue === visibilityOptions.PRIVATE) {
- // if changing away from private, make enabled features more permissive
- if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
- if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
- if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
- if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
- if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
- if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
- if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
- this.highlightChanges();
+ watch: {
+ visibilityLevel(value, oldValue) {
+ if (value === visibilityOptions.PRIVATE) {
+ // when private, features are restricted to "only team members"
+ this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
+ this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
+ this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
+ this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
+ this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
+ this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
+ if (this.pagesAccessLevel === 20) {
+ // When from Internal->Private narrow access for only members
+ this.pagesAccessLevel = 10;
}
- },
+ this.highlightChanges();
+ } else if (oldValue === visibilityOptions.PRIVATE) {
+ // if changing away from private, make enabled features more permissive
+ if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
+ if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
+ if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
+ if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
+ if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
+ if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
+ if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
+ this.highlightChanges();
+ }
+ },
- repositoryAccessLevel(value, oldValue) {
- if (value < oldValue) {
- // sub-features cannot have more premissive access level
- this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
- this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
+ repositoryAccessLevel(value, oldValue) {
+ if (value < oldValue) {
+ // sub-features cannot have more premissive access level
+ this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
+ this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
- if (value === 0) {
- this.containerRegistryEnabled = false;
- this.lfsEnabled = false;
- }
- } else if (oldValue === 0) {
- this.mergeRequestsAccessLevel = value;
- this.buildsAccessLevel = value;
- this.containerRegistryEnabled = true;
- this.lfsEnabled = true;
+ if (value === 0) {
+ this.containerRegistryEnabled = false;
+ this.lfsEnabled = false;
}
- },
+ } else if (oldValue === 0) {
+ this.mergeRequestsAccessLevel = value;
+ this.buildsAccessLevel = value;
+ this.containerRegistryEnabled = true;
+ this.lfsEnabled = true;
+ }
+ },
- issuesAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
- },
+ issuesAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
+ },
- mergeRequestsAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
- },
+ mergeRequestsAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
+ },
- buildsAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
- },
+ buildsAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
},
+ },
- methods: {
- highlightChanges() {
- this.highlightChangesClass = true;
- this.$nextTick(() => {
- this.highlightChangesClass = false;
- });
- },
+ methods: {
+ highlightChanges() {
+ this.highlightChangesClass = true;
+ this.$nextTick(() => {
+ this.highlightChangesClass = false;
+ });
+ },
- visibilityAllowed(option) {
- return this.allowedVisibilityOptions.includes(option);
- },
+ visibilityAllowed(option) {
+ return this.allowedVisibilityOptions.includes(option);
},
- };
+ },
+};
</script>
<template>
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
index 75cb6374ad5..f970a5ebb64 100644
--- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -1,8 +1,15 @@
<script>
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
+import { GlModal, GlModalDirective } from '@gitlab-org/gitlab-ui';
export default {
+ components: {
+ GlModal,
+ },
+ directives: {
+ 'gl-modal': GlModalDirective,
+ },
props: {
deleteWikiUrl: {
type: String,
@@ -54,7 +61,7 @@ export default {
>
{{ __('Delete') }}
</button>
- <gl-ui-modal
+ <gl-modal
:title="title"
:ok-title="s__('WikiPageConfirmDelete|Delete page')"
:modal-id="modalId"
@@ -81,6 +88,6 @@ export default {
name="authenticity_token"
/>
</form>
- </gl-ui-modal>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index c5a45afc634..8a0259ed5a5 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -1,6 +1,11 @@
<script>
+import { GlButton } from '@gitlab-org/gitlab-ui';
+
export default {
name: 'PipelinesEmptyState',
+ components: {
+ GlButton,
+ },
props: {
helpPagePath: {
type: String,
@@ -41,12 +46,13 @@ export default {
</p>
<div class="text-center">
- <a
+ <gl-button
:href="helpPagePath"
- class="btn btn-primary js-get-started-pipelines"
+ variant="primary"
+ class="js-get-started-pipelines"
>
{{ s__('Pipelines|Get started with Pipelines') }}
- </a>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 23c0be7742e..4de8b3401e8 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,10 +1,12 @@
<script>
import _ from 'underscore';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import StageColumnComponent from './stage_column_component.vue';
export default {
components: {
StageColumnComponent,
+ GlLoadingIcon,
},
props: {
isLoading: {
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index a1504592bbc..7cdde8a53b3 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -2,6 +2,8 @@
import ActionComponent from './action_component.vue';
import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
+import { sprintf } from '~/locale';
+import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@@ -36,6 +38,7 @@ export default {
directives: {
tooltip,
},
+ mixins: [delayedJobMixin],
props: {
job: {
type: Object,
@@ -52,6 +55,7 @@ export default {
default: Infinity,
},
},
+
computed: {
status() {
return this.job && this.job.status ? this.job.status : {};
@@ -59,17 +63,23 @@ export default {
tooltipText() {
const textBuilder = [];
+ const { name: jobName } = this.job;
- if (this.job.name) {
- textBuilder.push(this.job.name);
+ if (jobName) {
+ textBuilder.push(jobName);
}
- if (this.job.name && this.status.tooltip) {
+ const { tooltip: statusTooltip } = this.status;
+ if (jobName && statusTooltip) {
textBuilder.push('-');
}
- if (this.status.tooltip) {
- textBuilder.push(this.job.status.tooltip);
+ if (statusTooltip) {
+ if (this.isDelayedJob) {
+ textBuilder.push(sprintf(statusTooltip, { remainingTime: this.remainingTime }));
+ } else {
+ textBuilder.push(statusTooltip);
+ }
}
return textBuilder.join(' ');
@@ -88,6 +98,7 @@ export default {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
},
+
methods: {
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 1f9187c3d65..8f004b491c8 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub';
@@ -6,6 +7,7 @@ export default {
name: 'PipelineHeaderSection',
components: {
ciHeader,
+ GlLoadingIcon,
},
props: {
pipeline: {
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
index efb80d3a3c0..0911acbb131 100644
--- a/app/assets/javascripts/pipelines/components/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/nav_controls.vue
@@ -1,10 +1,13 @@
<script>
+import { GlLink, GlButton } from '@gitlab-org/gitlab-ui';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
export default {
name: 'PipelineNavControls',
components: {
LoadingButton,
+ GlLink,
+ GlButton,
},
props: {
newPipelinePath: {
@@ -40,28 +43,29 @@ export default {
</script>
<template>
<div class="nav-controls">
- <a
+ <gl-button
v-if="newPipelinePath"
:href="newPipelinePath"
- class="btn btn-success js-run-pipeline"
+ variant="success"
+ class="js-run-pipeline"
>
{{ s__('Pipelines|Run Pipeline') }}
- </a>
+ </gl-button>
<loading-button
v-if="resetCachePath"
:loading="isResetCacheButtonLoading"
:label="s__('Pipelines|Clear Runner Caches')"
- class="btn btn-default js-clear-cache"
+ class="js-clear-cache"
@click="onClickResetCache"
/>
- <a
+ <gl-button
v-if="ciLintPath"
:href="ciLintPath"
- class="btn btn-default js-ci-lint"
+ class="js-ci-lint"
>
{{ s__('Pipelines|CI Lint') }}
- </a>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 40df07650c9..be4b37f3c8c 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -1,14 +1,15 @@
<script>
-import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
-import popover from '../../vue_shared/directives/popover';
+import { GlLink, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import popover from '~/vue_shared/directives/popover';
export default {
components: {
- userAvatarLink,
+ UserAvatarLink,
+ GlLink,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
popover,
},
props: {
@@ -47,11 +48,12 @@ export default {
</script>
<template>
<div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags">
- <a
+ <gl-link
:href="pipeline.path"
- class="js-pipeline-url-link">
+ class="js-pipeline-url-link"
+ >
<span class="pipeline-id">#{{ pipeline.id }}</span>
- </a>
+ </gl-link>
<span>by</span>
<user-avatar-link
v-if="user"
@@ -68,36 +70,41 @@ export default {
<div class="label-container">
<span
v-if="pipeline.flags.latest"
- v-tooltip
+ v-gl-tooltip
class="js-pipeline-url-latest badge badge-success"
- title="Latest pipeline for this branch">
+ title="Latest pipeline for this branch"
+ >
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
- v-tooltip
+ v-gl-tooltip
:title="pipeline.yaml_errors"
- class="js-pipeline-url-yaml badge badge-danger">
+ class="js-pipeline-url-yaml badge badge-danger"
+ >
yaml invalid
</span>
<span
v-if="pipeline.flags.failure_reason"
- v-tooltip
+ v-gl-tooltip
:title="pipeline.failure_reason"
- class="js-pipeline-url-failure badge badge-danger">
+ class="js-pipeline-url-failure badge badge-danger"
+ >
error
</span>
- <a
+ <gl-link
v-if="pipeline.flags.auto_devops"
v-popover="popoverOptions"
tabindex="0"
class="js-pipeline-url-autodevops badge badge-info autodevops-badge"
- role="button">
+ role="button"
+ >
Auto DevOps
- </a>
+ </gl-link>
<span
v-if="pipeline.flags.stuck"
- class="js-pipeline-url-stuck badge badge-warning">
+ class="js-pipeline-url-stuck badge badge-warning"
+ >
stuck
</span>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index ea526cf1309..fcd8a54c9c1 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -155,14 +155,6 @@ export default {
);
},
- shouldRenderPagination() {
- return (
- !this.isLoading &&
- this.state.pipelines.length &&
- this.state.pageInfo.total > this.state.pageInfo.perPage
- );
- },
-
emptyTabMessage() {
const { scopes } = this.$options;
const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
@@ -232,36 +224,6 @@ export default {
this.setCommonData(resp.data.pipelines);
}
},
- /**
- * Handles URL and query parameter changes.
- * When the user uses the pagination or the tabs,
- * - update URL
- * - Make API request to the server with new parameters
- * - Update the polling function
- * - Update the internal state
- */
- updateContent(parameters) {
- this.updateInternalState(parameters);
-
- // fetch new data
- return this.service
- .getPipelines(this.requestData)
- .then(response => {
- this.isLoading = false;
- this.successCallback(response);
-
- // restart polling
- this.poll.restart({ data: this.requestData });
- })
- .catch(() => {
- this.isLoading = false;
- this.errorCallback();
-
- // restart polling
- this.poll.restart({ data: this.requestData });
- });
- },
-
handleResetRunnersCache(endpoint) {
this.isResetCacheButtonLoading = true;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index a7507fb3b6f..811495c45a9 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,17 +1,19 @@
<script>
+import { GlButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { s__, sprintf } from '~/locale';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
-import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
Icon,
GlCountdown,
+ GlButton,
+ GlLoadingIcon,
},
props: {
actions: {
@@ -29,7 +31,7 @@ export default {
if (action.scheduled_at) {
const confirmationMessage = sprintf(
s__(
- "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes.",
+ "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
),
{ jobName: action.name },
);
@@ -57,14 +59,12 @@ export default {
</script>
<template>
<div class="btn-group">
- <button
- v-tooltip
+ <gl-button
+ v-gl-tooltip
:disabled="isLoading"
- type="button"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
- data-placement="top"
aria-label="Manual job"
>
<icon
@@ -76,17 +76,16 @@ export default {
aria-hidden="true">
</i>
<gl-loading-icon v-if="isLoading" />
- </button>
+ </gl-button>
<ul class="dropdown-menu dropdown-menu-right">
<li
v-for="action in actions"
:key="action.path"
>
- <button
+ <gl-button
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
- type="button"
class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action)"
>
@@ -98,7 +97,7 @@ export default {
<icon name="clock" />
<gl-countdown :end-date-string="action.scheduled_at" />
</span>
- </button>
+ </gl-button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index e0f0434e03d..2abb24b87b6 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -1,13 +1,15 @@
<script>
-import tooltip from '../../vue_shared/directives/tooltip';
-import Icon from '../../vue_shared/components/icon.vue';
+import { GlLink, GlButton, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
Icon,
+ GlLink,
+ GlButton,
},
props: {
artifacts: {
@@ -22,11 +24,10 @@ export default {
class="btn-group"
role="group"
>
- <button
- v-tooltip
- class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
+ <gl-button
+ v-gl-tooltip
+ class="dropdown-toggle build-artifacts js-pipeline-dropdown-download"
title="Artifacts"
- data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts"
>
@@ -36,18 +37,19 @@ export default {
aria-hidden="true"
>
</i>
- </button>
+ </gl-button>
<ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(artifact, i) in artifacts"
- :key="i">
- <a
+ :key="i"
+ >
+ <gl-link
:href="artifact.path"
rel="nofollow"
download
>
Download {{ artifact.name }} artifacts
- </a>
+ </gl-link>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 7ec55792850..3df8f7a6da6 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -13,6 +13,7 @@
*/
import $ from 'jquery';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { __ } from '../../locale';
import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils';
@@ -26,6 +27,7 @@ export default {
components: {
Icon,
JobItem,
+ GlLoadingIcon,
},
directives: {
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 8929b397f6c..41bc5dcce5c 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,4 +1,5 @@
import Visibility from 'visibilityjs';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { __ } from '../../locale';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
@@ -13,6 +14,7 @@ export default {
PipelinesTableComponent,
SvgBlankState,
EmptyState,
+ GlLoadingIcon,
},
data() {
return {
@@ -23,6 +25,15 @@ export default {
hasMadeRequest: false,
};
},
+ computed: {
+ shouldRenderPagination() {
+ return (
+ !this.isLoading &&
+ this.state.pipelines.length &&
+ this.state.pageInfo.total > this.state.pageInfo.perPage
+ );
+ },
+ },
beforeMount() {
this.poll = new Poll({
resource: this.service,
@@ -65,6 +76,35 @@ export default {
this.poll.stop();
},
methods: {
+ /**
+ * Handles URL and query parameter changes.
+ * When the user uses the pagination or the tabs,
+ * - update URL
+ * - Make API request to the server with new parameters
+ * - Update the polling function
+ * - Update the internal state
+ */
+ updateContent(parameters) {
+ this.updateInternalState(parameters);
+
+ // fetch new data
+ return this.service
+ .getPipelines(this.requestData)
+ .then(response => {
+ this.isLoading = false;
+ this.successCallback(response);
+
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.errorCallback();
+
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ });
+ },
updateTable() {
// Cancel ongoing request
if (this.isMakingRequest) {
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
index d5266544307..f5dae5ad808 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
@@ -2,6 +2,7 @@ import _ from 'underscore';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import store from '../store';
@@ -11,6 +12,7 @@ export default {
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
+ GlLoadingIcon,
},
props: {
fieldId: {
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index ebe18b47e4e..998554d1be5 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -4,8 +4,10 @@ import { slugifyWithHyphens } from '../lib/utils/text_utility';
let hasUserDefinedProjectPath = false;
-const deriveProjectPathFromUrl = ($projectImportUrl) => {
- const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path');
+const deriveProjectPathFromUrl = $projectImportUrl => {
+ const $currentProjectPath = $projectImportUrl
+ .parents('.toggle-import-form')
+ .find('#project_path');
if (hasUserDefinedProjectPath) {
return;
}
@@ -52,9 +54,11 @@ const bindEvents = () => {
return;
}
- $('.how_to_import_link').on('click', (e) => {
+ $('.how_to_import_link').on('click', e => {
e.preventDefault();
- $(e.currentTarget).next('.modal').show();
+ $(e.currentTarget)
+ .next('.modal')
+ .show();
});
$('.modal-header .close').on('click', () => {
@@ -63,15 +67,21 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').on('click', () => {
const importHref = $('a.btn_import_gitlab_project').attr('href');
- $('.btn_import_gitlab_project')
- .attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&name=${$projectName.val()}&path=${$projectPath.val()}`);
+ $('.btn_import_gitlab_project').attr(
+ 'href',
+ `${importHref}?namespace_id=${$(
+ '#project_namespace_id',
+ ).val()}&name=${$projectName.val()}&path=${$projectPath.val()}`,
+ );
});
if ($pushNewProjectTipTrigger) {
$pushNewProjectTipTrigger
.removeAttr('rel')
.removeAttr('target')
- .on('click', (e) => { e.preventDefault(); })
+ .on('click', e => {
+ e.preventDefault();
+ })
.popover({
title: $pushNewProjectTipTrigger.data('title'),
placement: 'bottom',
@@ -79,13 +89,15 @@ const bindEvents = () => {
content: $('.push-new-project-tip-template').html(),
})
.on('shown.bs.popover', () => {
- $(document).on('click.popover touchstart.popover', (event) => {
+ $(document).on('click.popover touchstart.popover', event => {
if ($(event.target).closest('.popover').length === 0) {
$pushNewProjectTipTrigger.trigger('click');
}
});
- const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus');
+ const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find(
+ '.js-select-on-focus',
+ );
addSelectOnFocusBehaviour(target);
target.focus();
@@ -117,16 +129,18 @@ const bindEvents = () => {
const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text);
- $(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon);
+ $(selectedTemplate.icon)
+ .clone()
+ .addClass('d-block')
+ .appendTo($selectedIcon);
const $activeTabProjectName = $('.tab-pane.active #project_name');
const $activeTabProjectPath = $('.tab-pane.active #project_path');
$activeTabProjectName.focus();
- $activeTabProjectName
- .keyup(() => {
- onProjectNameChange($activeTabProjectName, $activeTabProjectPath);
- hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0;
- });
+ $activeTabProjectName.keyup(() => {
+ onProjectNameChange($activeTabProjectName, $activeTabProjectPath);
+ hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0;
+ });
}
$useTemplateBtn.on('change', chooseTemplate);
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index 120b4fc2f2b..9a729ca9b91 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -5,6 +5,7 @@ import Poll from '~/lib/utils/poll';
import Flash from '~/flash';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import CommitPipelineService from '../services/commit_pipeline_service';
export default {
@@ -13,6 +14,7 @@ export default {
},
components: {
ciIcon,
+ GlLoadingIcon,
},
props: {
endpoint: {
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index 9dd1c87a87d..0a906f40f5a 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Flash from '../../flash';
import store from '../stores';
import collapsibleContainer from './collapsible_container.vue';
@@ -9,6 +10,7 @@ export default {
name: 'RegistryListApp',
components: {
collapsibleContainer,
+ GlLoadingIcon,
},
props: {
endpoint: {
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 501b2625ae5..be9816a55c4 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Flash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -12,6 +13,7 @@ export default {
components: {
clipboardButton,
tableRegistry,
+ GlLoadingIcon,
},
directives: {
tooltip,
diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue
index 3b425ee2fed..f4243522ef8 100644
--- a/app/assets/javascripts/reports/components/issues_list.vue
+++ b/app/assets/javascripts/reports/components/issues_list.vue
@@ -1,18 +1,31 @@
<script>
-import IssuesBlock from '~/reports/components/report_issues.vue';
-import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants';
+import ReportItem from '~/reports/components/report_item.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
+
+const wrapIssueWithState = (status, isNew = false) => issue => ({
+ status: issue.status || status,
+ isNew,
+ issue,
+});
/**
* Renders block of issues
*/
-
export default {
components: {
- IssuesBlock,
+ SmartVirtualList,
+ ReportItem,
},
- success: STATUS_SUCCESS,
- failed: STATUS_FAILED,
- neutral: STATUS_NEUTRAL,
+ // Typical height of a report item in px
+ typicalReportItemHeight: 32,
+ /*
+ The maximum amount of shown issues. This is calculated by
+ ( max-height of report-block-list / typicalReportItemHeight ) + some safety margin
+ We will use VirtualList if we have more items than this number.
+ For entries lower than this number, the virtual scroll list calculates the total height of the element wrongly.
+ */
+ maxShownReportItems: 20,
props: {
newIssues: {
type: Array,
@@ -40,42 +53,34 @@ export default {
default: '',
},
},
+ computed: {
+ issuesWithState() {
+ return [
+ ...this.newIssues.map(wrapIssueWithState(STATUS_FAILED, true)),
+ ...this.unresolvedIssues.map(wrapIssueWithState(STATUS_FAILED)),
+ ...this.neutralIssues.map(wrapIssueWithState(STATUS_NEUTRAL)),
+ ...this.resolvedIssues.map(wrapIssueWithState(STATUS_SUCCESS)),
+ ];
+ },
+ },
};
</script>
<template>
- <div class="report-block-container">
-
- <issues-block
- v-if="newIssues.length"
- :component="component"
- :issues="newIssues"
- class="js-mr-code-new-issues"
- status="failed"
- is-new
- />
-
- <issues-block
- v-if="unresolvedIssues.length"
- :component="component"
- :issues="unresolvedIssues"
- :status="$options.failed"
- class="js-mr-code-new-issues"
- />
-
- <issues-block
- v-if="neutralIssues.length"
- :component="component"
- :issues="neutralIssues"
- :status="$options.neutral"
- class="js-mr-code-non-issues"
- />
-
- <issues-block
- v-if="resolvedIssues.length"
+ <smart-virtual-list
+ :length="issuesWithState.length"
+ :remain="$options.maxShownReportItems"
+ :size="$options.typicalReportItemHeight"
+ class="report-block-container"
+ wtag="ul"
+ wclass="report-block-list"
+ >
+ <report-item
+ v-for="(wrapped, index) in issuesWithState"
+ :key="index"
+ :issue="wrapped.issue"
+ :status="wrapped.status"
:component="component"
- :issues="resolvedIssues"
- :status="$options.success"
- class="js-mr-code-resolved-issues"
+ :is-new="wrapped.isNew"
/>
- </div>
+ </smart-virtual-list>
</template>
diff --git a/app/assets/javascripts/reports/components/report_issues.vue b/app/assets/javascripts/reports/components/report_item.vue
index a2a03945ae3..01e6d357a21 100644
--- a/app/assets/javascripts/reports/components/report_issues.vue
+++ b/app/assets/javascripts/reports/components/report_item.vue
@@ -3,14 +3,14 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { components, componentNames } from '~/reports/components/issue_body';
export default {
- name: 'ReportIssues',
+ name: 'ReportItem',
components: {
IssueStatusIcon,
...components,
},
props: {
- issues: {
- type: Array,
+ issue: {
+ type: Object,
required: true,
},
component: {
@@ -33,27 +33,21 @@ export default {
};
</script>
<template>
- <div>
- <ul class="report-block-list">
- <li
- v-for="(issue, index) in issues"
- :key="index"
- :class="{ 'is-dismissed': issue.isDismissed }"
- class="report-block-list-issue"
- >
- <issue-status-icon
- :status="issue.status || status"
- class="append-right-5"
- />
+ <li
+ :class="{ 'is-dismissed': issue.isDismissed }"
+ class="report-block-list-issue"
+ >
+ <issue-status-icon
+ :status="status"
+ class="append-right-5"
+ />
- <component
- :is="component"
- v-if="component"
- :issue="issue"
- :status="issue.status || status"
- :is-new="isNew"
- />
- </li>
- </ul>
- </div>
+ <component
+ :is="component"
+ v-if="component"
+ :issue="issue"
+ :status="status"
+ :is-new="isNew"
+ />
+ </li>
</template>
diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue
index 51188981bed..a44ba833b63 100644
--- a/app/assets/javascripts/reports/components/summary_row.vue
+++ b/app/assets/javascripts/reports/components/summary_row.vue
@@ -1,6 +1,7 @@
<script>
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
/**
* Renders the summary row for each report
@@ -15,6 +16,7 @@ export default {
components: {
CiIcon,
Popover,
+ GlLoadingIcon,
},
props: {
summary: {
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 6b3753f7966..225e21ad322 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -21,7 +21,7 @@ Sidebar.initialize = function(currentUser) {
}
};
-Sidebar.prototype.removeListeners = function () {
+Sidebar.prototype.removeListeners = function() {
this.sidebar.off('click', '.sidebar-collapsed-icon');
this.sidebar.off('hidden.gl.dropdown');
$('.dropdown').off('loading.gl.dropdown');
@@ -38,10 +38,12 @@ Sidebar.prototype.addEventListeners = function() {
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
- return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
+ return $(document)
+ .off('click', '.js-issuable-todo')
+ .on('click', '.js-issuable-todo', this.toggleTodo);
};
-Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
+Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault();
$this = $(this);
@@ -51,18 +53,26 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
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');
+ $('aside.right-sidebar')
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed');
+ $('.layout-page')
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed');
} else {
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
- $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
- $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ $('aside.right-sidebar')
+ .removeClass('right-sidebar-collapsed')
+ .addClass('right-sidebar-expanded');
+ $('.layout-page')
+ .removeClass('right-sidebar-collapsed')
+ .addClass('right-sidebar-expanded');
}
$this.attr('data-original-title', tooltipLabel);
if (!triggered) {
- Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
+ Cookies.set('collapsed_gutter', $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
};
@@ -71,21 +81,27 @@ Sidebar.prototype.toggleTodo = function(e) {
$this = $(e.currentTarget);
ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post';
if ($this.attr('data-delete-path')) {
- url = "" + ($this.attr('data-delete-path'));
+ url = '' + $this.attr('data-delete-path');
} else {
- url = "" + ($this.data('url'));
+ url = '' + $this.data('url');
}
$this.tooltip('hide');
- $('.js-issuable-todo').disable().addClass('is-loading');
+ $('.js-issuable-todo')
+ .disable()
+ .addClass('is-loading');
axios[ajaxType](url, {
issuable_id: $this.data('issuableId'),
issuable_type: $this.data('issuableType'),
- }).then(({ data }) => {
- this.todoUpdateDone(data);
- }).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`));
+ })
+ .then(({ data }) => {
+ this.todoUpdateDone(data);
+ })
+ .catch(() =>
+ flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`),
+ );
};
Sidebar.prototype.todoUpdateDone = function(data) {
@@ -99,7 +115,8 @@ Sidebar.prototype.todoUpdateDone = function(data) {
const $el = $(el);
const $elText = $el.find('.js-issuable-todo-inner');
- $el.removeClass('is-loading')
+ $el
+ .removeClass('is-loading')
.enable()
.attr('aria-label', $el.data(`${attrPrefix}Text`))
.attr('data-delete-path', deletePath)
@@ -119,7 +136,9 @@ Sidebar.prototype.todoUpdateDone = function(data) {
Sidebar.prototype.sidebarDropdownLoading = function(e) {
var $loading, $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ $sidebarCollapsedIcon = $(this)
+ .closest('.block')
+ .find('.sidebar-collapsed-icon');
img = $sidebarCollapsedIcon.find('img');
i = $sidebarCollapsedIcon.find('i');
$loading = $('<i class="fa fa-spinner fa-spin"></i>');
@@ -134,7 +153,9 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) {
Sidebar.prototype.sidebarDropdownLoaded = function(e) {
var $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ $sidebarCollapsedIcon = $(this)
+ .closest('.block')
+ .find('.sidebar-collapsed-icon');
img = $sidebarCollapsedIcon.find('img');
$sidebarCollapsedIcon.find('i.fa-spin').remove();
i = $sidebarCollapsedIcon.find('i');
@@ -220,7 +241,7 @@ Sidebar.prototype.isOpen = function() {
};
Sidebar.prototype.getBlock = function(name) {
- return this.sidebar.find(".block." + name);
+ return this.sidebar.find('.block.' + name);
};
export default Sidebar;
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index ad4f5320ff8..17def77b2d7 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -234,7 +234,9 @@ export class SearchAutocomplete {
icon,
text: term,
template,
- url: `${gon.relative_url_root}/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
+ url: `${
+ gon.relative_url_root
+ }/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
});
}
}
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index 43f0b6651b9..4d461baf74d 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import GfmAutoComplete from '~/gfm_auto_complete';
import { __, s__ } from '~/locale';
import Api from '~/api';
+import { GlModal, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
@@ -13,6 +14,10 @@ const emojiMenuClass = 'js-modal-status-emoji-menu';
export default {
components: {
Icon,
+ GlModal,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
props: {
currentEmoji: {
@@ -152,7 +157,7 @@ export default {
</script>
<template>
- <gl-ui-modal
+ <gl-modal
:title="s__('SetStatusModal|Set a status')"
:modal-id="modalId"
:ok-title="s__('SetStatusModal|Set status')"
@@ -237,5 +242,5 @@ export default {
</div>
</div>
</div>
- </gl-ui-modal>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index dd155c133ce..f1ea6aacdb2 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -74,8 +74,8 @@ export default {
}
if (!this.users.length) {
- const emptyTooltipLabel = this.issuableType === 'issue' ?
- __('Assignee(s)') : __('Assignee');
+ const emptyTooltipLabel =
+ this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee');
names.push(emptyTooltipLabel);
}
@@ -248,4 +248,3 @@ export default {
</div>
</div>
</template>
-
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 11b5dbe5f8e..fe73f6a0cef 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -2,6 +2,7 @@
import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
directives: {
@@ -9,6 +10,7 @@ export default {
},
components: {
userAvatarImage,
+ GlLoadingIcon,
},
props: {
loading: {
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 448c8fc3602..b6151aa6c64 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -1,74 +1,74 @@
<script>
- import { __ } from '~/locale';
- import icon from '~/vue_shared/components/icon.vue';
- import toggleButton from '~/vue_shared/components/toggle_button.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
- import eventHub from '../../event_hub';
+import { __ } from '~/locale';
+import icon from '~/vue_shared/components/icon.vue';
+import toggleButton from '~/vue_shared/components/toggle_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import eventHub from '../../event_hub';
- const ICON_ON = 'notifications';
- const ICON_OFF = 'notifications-off';
- const LABEL_ON = __('Notifications on');
- const LABEL_OFF = __('Notifications off');
+const ICON_ON = 'notifications';
+const ICON_OFF = 'notifications-off';
+const LABEL_ON = __('Notifications on');
+const LABEL_OFF = __('Notifications off');
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ toggleButton,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- components: {
- icon,
- toggleButton,
+ subscribed: {
+ type: Boolean,
+ required: false,
+ default: null,
},
- props: {
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
- subscribed: {
- type: Boolean,
- required: false,
- default: null,
- },
- id: {
- type: Number,
- required: false,
- default: null,
- },
+ id: {
+ type: Number,
+ required: false,
+ default: null,
},
- computed: {
- showLoadingState() {
- return this.subscribed === null;
- },
- notificationIcon() {
- return this.subscribed ? ICON_ON : ICON_OFF;
- },
- notificationTooltip() {
- return this.subscribed ? LABEL_ON : LABEL_OFF;
- },
+ },
+ computed: {
+ showLoadingState() {
+ return this.subscribed === null;
},
- 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);
+ notificationIcon() {
+ return this.subscribed ? ICON_ON : ICON_OFF;
+ },
+ notificationTooltip() {
+ return this.subscribed ? LABEL_ON : LABEL_OFF;
+ },
+ },
+ 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');
- },
+ // Component event emission.
+ this.$emit('toggleSubscription', this.id);
+ },
+ onClickCollapsedIcon() {
+ this.$emit('toggleSidebar');
},
- };
+ },
+};
</script>
<template>
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 e74912d628f..b145e5dc5e2 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -1,9 +1,13 @@
<script>
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip';
+import { GlProgressBar } from '@gitlab-org/gitlab-ui';
export default {
name: 'TimeTrackingComparisonPane',
+ components: {
+ GlProgressBar,
+ },
directives: {
tooltip,
},
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index bc59774f0a8..913a616d9f1 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -1,6 +1,7 @@
<script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
@@ -13,6 +14,7 @@ export default {
},
components: {
Icon,
+ GlLoadingIcon,
},
props: {
issuableId: {
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index d9ca5e46770..3e040ec8428 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -39,9 +39,10 @@ export default class SidebarMediator {
}
fetch() {
- return this.service.get()
+ return this.service
+ .get()
.then(response => response.json())
- .then((data) => {
+ .then(data => {
this.processFetchedData(data);
})
.catch(() => new Flash('Error occurred when fetching sidebar data'));
@@ -56,30 +57,33 @@ export default class SidebarMediator {
toggleSubscription() {
this.store.setFetchingState('subscriptions', true);
- return this.service.toggleSubscription()
+ return this.service
+ .toggleSubscription()
.then(() => {
this.store.setSubscribedState(!this.store.subscribed);
this.store.setFetchingState('subscriptions', false);
})
- .catch((err) => {
+ .catch(err => {
this.store.setFetchingState('subscriptions', false);
throw err;
});
}
fetchAutocompleteProjects(searchTerm) {
- return this.service.getProjectsAutocomplete(searchTerm)
+ return this.service
+ .getProjectsAutocomplete(searchTerm)
.then(response => response.json())
- .then((data) => {
+ .then(data => {
this.store.setAutocompleteProjects(data);
return this.store.autocompleteProjects;
});
}
moveIssue() {
- return this.service.moveIssue(this.store.moveToProjectId)
+ return this.service
+ .moveIssue(this.store.moveToProjectId)
.then(response => response.json())
- .then((data) => {
+ .then(data => {
if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 4b090212d83..ce051582299 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
+/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
/* global Issuable */
/* global emitSidebarEvent */
@@ -696,17 +696,21 @@ UsersSelect.prototype.formatResult = function(user) {
} else {
avatar = gon.default_avatar_url;
}
- return (
- "<div class='user-result " +
- (!user.username ? 'no-username' : void 0) +
- "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" +
- avatar +
- "'></div> <div class='user-name dropdown-menu-user-full-name'>" +
- _.escape(user.name) +
- "</div> <div class='user-username dropdown-menu-user-username'>" +
- (!user.invite ? '@' + _.escape(user.username) : '') +
- '</div> </div>'
- );
+ return `
+ <div class='user-result'>
+ <div class='user-image'>
+ <img class='avatar avatar-inline s32' src='${avatar}'>
+ </div>
+ <div class='user-info'>
+ <div class='user-name dropdown-menu-user-full-name'>
+ ${_.escape(user.name)}
+ </div>
+ <div class='user-username dropdown-menu-user-username text-secondary'>
+ ${!user.invite ? '@' + _.escape(user.username) : ''}
+ </div>
+ </div>
+ </div>
+ `;
};
UsersSelect.prototype.formatSelection = function(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 57c52a2016a..2a8380f5f2b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -65,6 +65,14 @@ export default {
deployedText() {
return this.$options.deployedTextMap[this.deployment.status];
},
+ isDeployInProgress() {
+ return this.deployment.status === 'running';
+ },
+ deployInProgressTooltip() {
+ return this.isDeployInProgress
+ ? __('Stopping this environment is currently not possible as a deployment is in progress')
+ : '';
+ },
shouldRenderDropdown() {
return (
this.enableCiEnvironmentsStatusChanges &&
@@ -183,15 +191,23 @@ export default {
css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin"
/>
</template>
- <loading-button
+ <span
v-if="deployment.stop_url"
- :loading="isStopping"
- container-class="btn btn-default btn-sm inline prepend-left-4"
- title="Stop environment"
- @click="stopEnvironment"
+ v-tooltip
+ :title="deployInProgressTooltip"
+ class="d-inline-block"
+ tabindex="0"
>
- <icon name="stop" />
- </loading-button>
+ <loading-button
+ :loading="isStopping"
+ :disabled="isDeployInProgress"
+ :title="__('Stop environment')"
+ container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4"
+ @click="stopEnvironment"
+ >
+ <icon name="stop" />
+ </loading-button>
+ </span>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index fee41b239e8..53608838f2f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -1,5 +1,6 @@
<script>
/* eslint-disable vue/require-default-prop */
+import { sprintf, __ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
@@ -36,6 +37,10 @@ export default {
type: String,
required: false,
},
+ troubleshootingDocsPath: {
+ type: String,
+ required: true,
+ },
},
computed: {
hasPipeline() {
@@ -57,6 +62,18 @@ export default {
hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
},
+ errorText() {
+ return sprintf(
+ __(
+ 'Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}',
+ ),
+ {
+ linkStart: `<a href="${this.troubleshootingDocsPath}">`,
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
},
};
</script>
@@ -77,8 +94,10 @@ export default {
name="status_failed_borderless"
/>
</div>
- <div class="media-body">
- Could not connect to the CI server. Please check your settings and try again
+ <div
+ class="media-body"
+ v-html="errorText"
+ >
</div>
</template>
<template v-else-if="hasPipeline">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index ba6a1687e51..b3340290ed3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -1,9 +1,11 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
export default {
components: {
ciIcon,
+ GlLoadingIcon,
},
props: {
status: {
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 4f8b07484c0..4bfbdcf1404 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
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -6,6 +7,7 @@ export default {
name: 'MRWidgetAutoMergeFailed',
components: {
statusIcon,
+ GlLoadingIcon,
},
props: {
mr: {
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 656c3b5c47e..7e33021e4b4 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
@@ -6,6 +6,7 @@ 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';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
name: 'MRWidgetMerged',
@@ -16,6 +17,7 @@ export default {
MrWidgetAuthorTime,
statusIcon,
ClipboardButton,
+ GlLoadingIcon,
},
props: {
mr: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 041fa13a8f5..0e714cc2aa1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -8,6 +9,7 @@ export default {
name: 'MRWidgetRebase',
components: {
statusIcon,
+ GlLoadingIcon,
},
props: {
mr: {
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 c8ad2aa30a6..e7baecbcde4 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
@@ -71,7 +71,12 @@ export default {
return defaultClass;
},
iconClass() {
- if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) {
+ if (
+ this.status === 'failed' ||
+ !this.commitMessage.length ||
+ !this.mr.isMergeAllowed ||
+ this.mr.preventMerge
+ ) {
return 'warning';
}
return 'success';
@@ -90,10 +95,12 @@ export default {
},
isMergeButtonDisabled() {
const { commitMessage } = this;
- return Boolean(!commitMessage.length
- || !this.shouldShowMergeControls()
- || this.isMakingRequest
- || this.mr.preventMerge);
+ return Boolean(
+ !commitMessage.length ||
+ !this.shouldShowMergeControls() ||
+ this.isMakingRequest ||
+ this.mr.preventMerge,
+ );
},
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled;
@@ -140,9 +147,10 @@ export default {
};
this.isMakingRequest = true;
- this.service.merge(options)
+ this.service
+ .merge(options)
.then(res => res.data)
- .then((data) => {
+ .then(data => {
const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
if (data.status === 'merge_when_pipeline_succeeds') {
@@ -167,9 +175,10 @@ export default {
});
},
handleMergePolling(continuePolling, stopPolling) {
- this.service.poll()
+ this.service
+ .poll()
.then(res => res.data)
- .then((data) => {
+ .then(data => {
if (data.state === 'merged') {
// If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested');
@@ -205,9 +214,10 @@ export default {
});
},
handleRemoveBranchPolling(continuePolling, stopPolling) {
- this.service.poll()
+ this.service
+ .poll()
.then(res => res.data)
- .then((data) => {
+ .then(data => {
// If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time
if (data.source_branch_exists) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index a5c69d2bc7a..063d1e15544 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -116,7 +116,7 @@ export default {
// init polling
this.initPostMergeDeploymentsPolling();
}
- }
+ },
},
created() {
this.initPolling();
@@ -213,17 +213,21 @@ export default {
})
.catch(() => this.throwDeploymentsError());
},
- fetchPostMergeDeployments(){
+ fetchPostMergeDeployments() {
return this.fetchDeployments('merge_commit')
.then(({ data }) => {
if (data.length) {
this.mr.postMergeDeployments = data;
}
})
- .catch(() => this.throwDeploymentsError());
+ .catch(() => this.throwDeploymentsError());
},
throwDeploymentsError() {
- createFlash(__('Something went wrong while fetching the environments for this merge request. Please try again.'));
+ createFlash(
+ __(
+ 'Something went wrong while fetching the environments for this merge request. Please try again.',
+ ),
+ );
},
fetchActionsContent() {
this.service
@@ -301,6 +305,7 @@ export default {
:has-ci="mr.hasCI"
:source-branch="mr.sourceBranch"
:source-branch-link="mr.sourceBranchLink"
+ :troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
<deployment
v-for="deployment in mr.deployments"
@@ -355,6 +360,7 @@ export default {
:has-ci="mr.hasCI"
:source-branch="mr.targetBranch"
:source-branch-link="mr.targetBranch"
+ :troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
<deployment
v-for="postMergeDeployment in mr.postMergeDeployments"
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index bf5b85b2ae6..0bb70bfd658 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -24,8 +24,8 @@ export default class MRWidgetService {
fetchDeployments(targetParam) {
return axios.get(this.endpoints.ciEnvironmentsStatusPath, {
params: {
- environment_target: targetParam
- }
+ environment_target: targetParam,
+ },
});
}
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 a0c008e7314..5c9a7133a6e 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
@@ -18,6 +18,7 @@ export default class MergeRequestStore {
this.squash = data.squash;
this.squashBeforeMergeHelpPath =
this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path;
+ this.troubleshootingDocsPath = this.troubleshootingDocsPath || data.troubleshooting_docs_path;
this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true;
this.iid = data.iid;
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 8684005e0fb..766fc211bf5 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -1,5 +1,5 @@
<script>
-import tooltip from '~/vue_shared/directives/tooltip';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
@@ -10,7 +10,7 @@ export default {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
file: {
@@ -79,10 +79,8 @@ export default {
<template>
<span
- v-tooltip
+ v-gl-tooltip.right
:title="tooltipTitle"
- data-container="body"
- data-placement="right"
class="file-changed-icon ml-auto"
>
<icon
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 c60052fec50..6780254827f 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,6 +1,6 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
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.
@@ -27,7 +27,7 @@ export default {
CiIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
status: {
@@ -50,7 +50,7 @@ export default {
</script>
<template>
<a
- v-tooltip
+ v-gl-tooltip
:href="status.details_path"
:class="cssClass"
:title="!showText ? status.text : ''"
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index 945a33d9622..6b90a1f540e 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -12,20 +12,18 @@
* css-class="btn-transparent"
* />
*/
-import tooltip from '../directives/tooltip';
+import { GlButton, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '../components/icon.vue';
export default {
name: 'ClipboardButton',
-
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
-
components: {
+ GlButton,
Icon,
},
-
props: {
text: {
type: String,
@@ -68,16 +66,12 @@ export default {
</script>
<template>
- <button
- v-tooltip
+ <gl-button
+ v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
:class="cssClass"
:title="title"
:data-clipboard-text="clipboardText"
- :data-container="tooltipContainer"
- :data-placement="tooltipPlacement"
- type="button"
- class="btn"
>
<icon name="duplicate" />
- </button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 13bca99dcb3..b1139f34e41 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,11 +1,11 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
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,
+ GlTooltip: GlTooltipDirective,
},
components: {
UserAvatarLink,
@@ -13,7 +13,7 @@ export default {
},
props: {
/**
- * Indicates the existance of a tag.
+ * Indicates the existence 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
*/
@@ -124,11 +124,10 @@ export default {
</div>
<a
- v-tooltip
+ v-gl-tooltip
:href="commitRef.ref_url"
:title="commitRef.name"
class="ref-name"
- data-container="body"
>
{{ commitRef.name }}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 8163947cd0c..6f2f0f98690 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -17,19 +17,37 @@ export default {
type: Boolean,
default: true,
},
+ innerCssClasses: {
+ type: [Array, Object, String],
+ required: false,
+ default: '',
+ },
},
data() {
return {
width: 0,
height: 0,
- isZoomable: false,
- isZoomed: false,
+ isLoaded: false,
};
},
computed: {
fileSizeReadable() {
return numberToHumanSize(this.fileSize);
},
+ dimensionStyles() {
+ if (!this.isLoaded) return {};
+
+ return {
+ width: `${this.width}px`,
+ height: `${this.height}px`,
+ };
+ },
+ hasFileSize() {
+ return this.fileSize > 0;
+ },
+ hasDimensions() {
+ return this.width && this.height;
+ },
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
@@ -48,51 +66,52 @@ export default {
const { contentImg } = this.$refs;
if (contentImg) {
- this.isZoomable =
- contentImg.naturalWidth > contentImg.width ||
- contentImg.naturalHeight > contentImg.height;
-
this.width = contentImg.naturalWidth;
this.height = contentImg.naturalHeight;
- this.$emit('imgLoaded', {
- width: this.width,
- height: this.height,
- renderedWidth: contentImg.clientWidth,
- renderedHeight: contentImg.clientHeight,
+ this.$nextTick(() => {
+ this.isLoaded = true;
+
+ this.$emit('imgLoaded', {
+ width: this.width,
+ height: this.height,
+ renderedWidth: contentImg.clientWidth,
+ renderedHeight: contentImg.clientHeight,
+ });
});
}
},
- onImgClick() {
- if (this.isZoomable) this.isZoomed = !this.isZoomed;
- },
},
};
</script>
<template>
- <div class="file-container">
- <div class="file-content image_file">
+ <div>
+ <div
+ :class="innerCssClasses"
+ :style="dimensionStyles"
+ class="position-relative"
+ >
<img
ref="contentImg"
- :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
:src="path"
- :alt="path"
@load="onImgLoad"
- @click="onImgClick"/>
- <p
- v-if="renderInfo"
- class="file-info prepend-top-10">
- <template v-if="fileSize>0">
- {{ fileSizeReadable }}
- </template>
- <template v-if="fileSize>0 && width && height">
- |
- </template>
- <template v-if="width && height">
- W: {{ width }} | H: {{ height }}
- </template>
- </p>
+ />
+ <slot name="image-overlay"></slot>
</div>
+ <p
+ v-if="renderInfo"
+ class="image-info"
+ >
+ <template v-if="hasFileSize">
+ {{ fileSizeReadable }}
+ </template>
+ <template v-if="hasFileSize && hasDimensions">
+ |
+ </template>
+ <template v-if="hasDimensions">
+ <strong>W</strong>: {{ width }} | <strong>H</strong>: {{ height }}
+ </template>
+ </p>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index cfc5343217c..9c3f3e7f7a9 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -69,6 +69,13 @@ export default {
:new-path="fullNewPath"
:old-path="fullOldPath"
:project-path="projectPath"
- />
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </component>
+ <slot></slot>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
index 38e881d17a2..cd0c1e850af 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
@@ -15,11 +15,6 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
@@ -120,7 +115,6 @@ export default {
key="onionOldImg"
:render-info="false"
:path="oldPath"
- :project-path="projectPath"
@imgLoaded="onionOldImgLoaded"
/>
</div>
@@ -136,9 +130,14 @@ export default {
key="onionNewImg"
:render-info="false"
:path="newPath"
- :project-path="projectPath"
@imgLoaded="onionNewImgLoaded"
- />
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
</div>
<div class="controls">
<div class="transparent"></div>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
index 86366c799a2..c3cfe54eb4d 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
@@ -16,11 +16,6 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
@@ -117,16 +112,14 @@ export default {
'height': swipeMaxPixelHeight,
}"
class="swipe-frame">
- <div class="frame deleted">
- <image-viewer
- key="swipeOldImg"
- ref="swipeOldImg"
- :render-info="false"
- :path="oldPath"
- :project-path="projectPath"
- @imgLoaded="swipeOldImgLoaded"
- />
- </div>
+ <image-viewer
+ key="swipeOldImg"
+ ref="swipeOldImg"
+ :render-info="false"
+ :path="oldPath"
+ class="frame deleted"
+ @imgLoaded="swipeOldImgLoaded"
+ />
<div
ref="swipeWrap"
:style="{
@@ -134,15 +127,19 @@ export default {
'height': swipeMaxPixelHeight,
}"
class="swipe-wrap">
- <div class="frame added">
- <image-viewer
- key="swipeNewImg"
- :render-info="false"
- :path="newPath"
- :project-path="projectPath"
- @imgLoaded="swipeNewImgLoaded"
- />
- </div>
+ <image-viewer
+ key="swipeNewImg"
+ :render-info="false"
+ :path="newPath"
+ class="frame added"
+ @imgLoaded="swipeNewImgLoaded"
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
</div>
<span
ref="swipeBar"
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
index 9c19266ecdf..9806d65e940 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
@@ -14,28 +14,29 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
};
</script>
<template>
- <div class="two-up view row">
- <div class="col-sm-6 frame deleted">
- <image-viewer
- :path="oldPath"
- :project-path="projectPath"
- />
- </div>
- <div class="col-sm-6 frame added">
- <image-viewer
- :path="newPath"
- :project-path="projectPath"
- />
- </div>
+ <div class="two-up view">
+ <image-viewer
+ :path="oldPath"
+ :render-info="true"
+ inner-css-classes="frame deleted"
+ class="wrap"
+ />
+ <image-viewer
+ :path="newPath"
+ :render-info="true"
+ :inner-css-classes="['frame', 'added']"
+ class="wrap"
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
index 1af85283277..e68a2aa73fa 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
@@ -8,9 +8,6 @@ import { diffModes, imageViewMode } from '../constants';
export default {
components: {
ImageViewer,
- TwoUpViewer,
- SwipeViewer,
- OnionSkinViewer,
},
props: {
diffMode: {
@@ -25,17 +22,32 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
mode: imageViewMode.twoup,
};
},
+ computed: {
+ imageViewComponent() {
+ switch (this.mode) {
+ case imageViewMode.twoup:
+ return TwoUpViewer;
+ case imageViewMode.swipe:
+ return SwipeViewer;
+ case imageViewMode.onion:
+ return OnionSkinViewer;
+ default:
+ return undefined;
+ }
+ },
+ isNew() {
+ return this.diffMode === diffModes.new;
+ },
+ imagePath() {
+ return this.isNew ? this.newPath : this.oldPath;
+ },
+ },
methods: {
changeMode(newMode) {
this.mode = newMode;
@@ -52,15 +64,16 @@ export default {
v-if="diffMode === $options.diffModes.replaced"
class="diff-viewer">
<div class="image js-replaced-image">
- <two-up-viewer
- v-if="mode === $options.imageViewMode.twoup"
- v-bind="$props"/>
- <swipe-viewer
- v-else-if="mode === $options.imageViewMode.swipe"
- v-bind="$props"/>
- <onion-skin-viewer
- v-else-if="mode === $options.imageViewMode.onion"
- v-bind="$props"/>
+ <component
+ :is="imageViewComponent"
+ v-bind="$props"
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </component>
</div>
<div class="view-modes">
<ul class="view-modes-menu">
@@ -87,23 +100,27 @@ export default {
</li>
</ul>
</div>
- <div class="note-container"></div>
- </div>
- <div
- v-else-if="diffMode === $options.diffModes.new"
- class="diff-viewer added">
- <image-viewer
- :path="newPath"
- :project-path="projectPath"
- />
</div>
<div
v-else
- class="diff-viewer deleted">
- <image-viewer
- :path="oldPath"
- :project-path="projectPath"
- />
+ class="diff-viewer"
+ >
+ <div class="image">
+ <image-viewer
+ :path="imagePath"
+ :inner-css-classes="['frame', {
+ 'added': isNew,
+ 'deleted': diffMode === $options.diffModes.deleted
+ }]"
+ >
+ <slot
+ v-if="isNew"
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index 31087017968..0e194eaaed5 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -1,7 +1,11 @@
<script>
import { __ } from '~/locale';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
isDisabled: {
type: Boolean,
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 408f7d7965f..03818be6a69 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import getIconForFile from './file_icon/file_icon_map';
import icon from '../../vue_shared/components/icon.vue';
@@ -17,6 +18,7 @@ import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
icon,
+ GlLoadingIcon,
},
props: {
fileName: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
index 460fa6ad72e..388a2f4ca36 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
@@ -56,12 +56,14 @@ export default {
filteredResults() {
if (this.filter !== '') {
return this.items.filter(
- item => item[this.filterKey] && item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()),
+ item =>
+ item[this.filterKey] &&
+ item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()),
);
}
return this.items.slice(0, this.visibleItems);
- }
+ },
},
mounted() {
/**
diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
index 9327a2a4a6c..a35986b2d03 100644
--- a/app/assets/javascripts/vue_shared/components/gl_countdown.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
@@ -1,10 +1,14 @@
<script>
import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
/**
* Counts down to a given end date.
*/
export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
endDateString: {
type: String,
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 26f9d5ddc91..a25841fc02f 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -1,6 +1,6 @@
<script>
// only allow classes in images.scss e.g. s12
-const validSizes = [8, 10, 12, 16, 18, 24, 32, 48, 72];
+const validSizes = [8, 10, 12, 14, 16, 18, 24, 32, 48, 72];
let iconValidator = () => true;
/*
@@ -8,7 +8,7 @@ let iconValidator = () => true;
*/
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line global-require
- const data = require('@gitlab-org/gitlab-svgs/dist/icons.json');
+ const data = require('@gitlab/svgs/dist/icons.json');
const { icons } = data;
iconValidator = value => {
if (icons.includes(value)) {
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index f9b7fd5b1f9..69d7e5c46f5 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
/* eslint-disable vue/require-default-prop */
/* This is a re-usable vue component for rendering a button
that will probably be sending off ajax requests and need
@@ -18,6 +19,9 @@
*/
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
loading: {
type: Boolean,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 3ddb39730c4..27e3f314dd3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,17 +1,17 @@
<script>
import $ from 'jquery';
-import Tooltip from '../../directives/tooltip';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import ToolbarButton from './toolbar_button.vue';
import Icon from '../icon.vue';
export default {
- directives: {
- Tooltip,
- },
components: {
ToolbarButton,
Icon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
previewMarkdown: {
type: Boolean,
@@ -147,7 +147,7 @@ export default {
icon="table"
/>
<button
- v-tooltip
+ v-gl-tooltip
aria-label="Go full screen"
class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
data-container="body"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 3e89e1c1e75..91d0bbfc21c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -1,13 +1,13 @@
<script>
-import tooltip from '../../directives/tooltip';
-import icon from '../icon.vue';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import Icon from '../icon.vue';
export default {
components: {
- icon,
+ Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
buttonTitle: {
@@ -43,7 +43,7 @@ export default {
<template>
<button
- v-tooltip
+ v-gl-tooltip
:data-md-tag="tag"
:data-md-select="tagSelect"
:data-md-block="tagBlock"
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index f56414c3c63..8cb72afcdc0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -10,7 +10,7 @@ export default {
</script>
<template>
- <li class="timeline-entry note">
+ <li class="timeline-entry note note-wrapper">
<div class="timeline-entry-inner">
<div class="timeline-icon">
</div>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index de3c7a80365..6a44e6a29ed 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -76,7 +76,7 @@ export default {
<li
:id="noteAnchorId"
:class="{ target: isTargetNote }"
- class="note system-note timeline-entry">
+ class="note system-note timeline-entry note-wrapper">
<div class="timeline-entry-inner">
<div
class="timeline-icon"
diff --git a/app/assets/javascripts/vue_shared/components/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue
index 1f2a679c145..89dcf049f6e 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_links.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue
@@ -1,7 +1,11 @@
<script>
+import { GlPagination } from '@gitlab-org/gitlab-ui';
import { s__ } from '../../locale';
export default {
+ components: {
+ GlPagination,
+ },
props: {
change: {
type: Function,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
index 11fac3bb12c..5841db52704 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -1,39 +1,39 @@
<script>
- import tooltip from '~/vue_shared/directives/tooltip';
+import tooltip from '~/vue_shared/directives/tooltip';
- export default {
- name: 'CollapsedCalendarIcon',
- directives: {
- tooltip,
+export default {
+ name: 'CollapsedCalendarIcon',
+ directives: {
+ tooltip,
+ },
+ props: {
+ containerClass: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- containerClass: {
- type: String,
- required: false,
- default: '',
- },
- text: {
- type: String,
- required: false,
- default: '',
- },
- showIcon: {
- type: Boolean,
- required: false,
- default: true,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
+ text: {
+ type: String,
+ required: false,
+ default: '',
},
- methods: {
- click() {
- this.$emit('click');
- },
+ showIcon: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- };
+ tooltipText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ methods: {
+ click() {
+ this.$emit('click');
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
index 6e7194ccc9e..174c29809ac 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
@@ -1,84 +1,79 @@
<script>
- import { __ } from '~/locale';
- import timeagoMixin from '~/vue_shared/mixins/timeago';
- import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
- import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
+import { __ } from '~/locale';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
+import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
- export default {
- name: 'SidebarCollapsedGroupedDatePicker',
- components: {
- collapsedCalendarIcon,
+export default {
+ name: 'SidebarCollapsedGroupedDatePicker',
+ components: {
+ collapsedCalendarIcon,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- collapsed: {
- type: Boolean,
- required: false,
- default: true,
- },
- minDate: {
- type: Date,
- required: false,
- default: null,
- },
- maxDate: {
- type: Date,
- required: false,
- default: null,
- },
- disableClickableIcons: {
- type: Boolean,
- required: false,
- default: false,
- },
+ minDate: {
+ type: Date,
+ required: false,
+ default: null,
},
- computed: {
- hasMinAndMaxDates() {
- return this.minDate && this.maxDate;
- },
- hasNoMinAndMaxDates() {
- return !this.minDate && !this.maxDate;
- },
- showMinDateBlock() {
- return this.minDate || this.hasNoMinAndMaxDates;
- },
- showFromText() {
- return !this.maxDate && this.minDate;
- },
- iconClass() {
- const disabledClass = this.disableClickableIcons ? 'disabled' : '';
- return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
- },
+ maxDate: {
+ type: Date,
+ required: false,
+ default: null,
},
- methods: {
- toggleSidebar() {
- this.$emit('toggleCollapse');
- },
- dateText(dateType = 'min') {
- const date = this[`${dateType}Date`];
- const dateWords = dateInWords(date, true);
- const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
+ disableClickableIcons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ hasMinAndMaxDates() {
+ return this.minDate && this.maxDate;
+ },
+ hasNoMinAndMaxDates() {
+ return !this.minDate && !this.maxDate;
+ },
+ showMinDateBlock() {
+ return this.minDate || this.hasNoMinAndMaxDates;
+ },
+ showFromText() {
+ return !this.maxDate && this.minDate;
+ },
+ iconClass() {
+ const disabledClass = this.disableClickableIcons ? 'disabled' : '';
+ return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
+ },
+ },
+ methods: {
+ toggleSidebar() {
+ this.$emit('toggleCollapse');
+ },
+ dateText(dateType = 'min') {
+ const date = this[`${dateType}Date`];
+ const dateWords = dateInWords(date, true);
+ const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
- return date ? parsedDateWords : __('None');
- },
- tooltipText(dateType = 'min') {
- const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
- const date = this[`${dateType}Date`];
- const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date);
- const dateText = date ? [
- this.dateText(dateType),
- `(${timeAgo})`,
- ].join(' ') : '';
+ return date ? parsedDateWords : __('None');
+ },
+ tooltipText(dateType = 'min') {
+ const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
+ const date = this[`${dateType}Date`];
+ const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date);
+ const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : '';
- if (date) {
- return [defaultText, dateText].join('<br />');
- }
- return __('Start and due date');
- },
+ if (date) {
+ return [defaultText, dateText].join('<br />');
+ }
+ return __('Start and due date');
},
- };
+ },
+};
</script>
<template>
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 500586302cf..5b12bb6b59e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import datePicker from '../pikaday.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
@@ -10,6 +11,7 @@ export default {
datePicker,
toggleSidebar,
collapsedCalendarIcon,
+ GlLoadingIcon,
},
props: {
blockClass: {
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 3df286de129..e50d612ce36 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
@@ -4,6 +4,7 @@ import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
@@ -24,6 +25,7 @@ export default {
DropdownSearchInput,
DropdownFooter,
DropdownCreateLabel,
+ GlLoadingIcon,
},
props: {
showCreate: {
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 af297f3c408..0d5fc07e6e3 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
@@ -14,7 +14,10 @@ export default {
},
computed: {
labelsList() {
- const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', ');
+ const labelsString = this.labels
+ .slice(0, 5)
+ .map(label => label.title)
+ .join(', ');
if (this.labels.length > 5) {
return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
diff --git a/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue
new file mode 100644
index 00000000000..63034a45f77
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue
@@ -0,0 +1,42 @@
+<script>
+import VirtualList from 'vue-virtual-scroll-list';
+
+export default {
+ name: 'SmartVirtualList',
+ components: { VirtualList },
+ props: {
+ size: { type: Number, required: true },
+ length: { type: Number, required: true },
+ remain: { type: Number, required: true },
+ rtag: { type: String, default: 'div' },
+ wtag: { type: String, default: 'div' },
+ wclass: { type: String, default: null },
+ },
+};
+</script>
+<template>
+ <virtual-list
+ v-if="length > remain"
+ v-bind="$attrs"
+ :size="remain"
+ :remain="remain"
+ :rtag="rtag"
+ :wtag="wtag"
+ :wclass="wclass"
+ class="js-virtual-list"
+ >
+ <slot></slot>
+ </virtual-list>
+ <component
+ :is="rtag"
+ v-else
+ class="js-plain-element"
+ >
+ <component
+ :is="wtag"
+ :class="wclass"
+ >
+ <slot></slot>
+ </component>
+ </component>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 4e9289cbed8..e7cb5cfac12 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { s__ } from '../../locale';
import icon from './icon.vue';
@@ -10,6 +11,7 @@ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
export default {
components: {
icon,
+ GlLoadingIcon,
},
model: {
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index 7737b9f2697..4cfb1ded0a9 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -15,14 +15,14 @@
*/
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '../../../lazy_loader';
-import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarImage',
- directives: {
- tooltip,
+ components: {
+ GlTooltip,
},
props: {
lazy: {
@@ -73,9 +73,6 @@ export default {
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
},
- tooltipContainer() {
- return this.tooltipText ? 'body' : null;
- },
avatarSizeClass() {
return `s${this.size}`;
},
@@ -84,22 +81,30 @@ export default {
</script>
<template>
- <img
- v-tooltip
- :class="{
- lazy: lazy,
- [avatarSizeClass]: true,
- [cssClasses]: true
- }"
- :src="resultantSrcAttribute"
- :width="size"
- :height="size"
- :alt="imgAlt"
- :data-src="sanitizedSource"
- :data-container="tooltipContainer"
- :data-placement="tooltipPlacement"
- :title="tooltipText"
- class="avatar"
- data-boundary="window"
- />
+ <span>
+ <img
+ ref="userAvatarImage"
+ :class="{
+ lazy: lazy,
+ [avatarSizeClass]: true,
+ [cssClasses]: true
+ }"
+ :src="resultantSrcAttribute"
+ :width="size"
+ :height="size"
+ :alt="imgAlt"
+ :data-src="sanitizedSource"
+ class="avatar"
+ />
+ <gl-tooltip
+ :target="() => $refs.userAvatarImage"
+ :placement="tooltipPlacement"
+ boundary="window"
+ class="js-user-avatar-image-toolip"
+ >
+ <slot>
+ {{ tooltipText }}
+ </slot>
+ </gl-tooltip>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 86c7498a092..351a639c6e8 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -17,9 +17,8 @@
*/
-import { GlLink } from '@gitlab-org/gitlab-ui';
+import { GlLink, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import userAvatarImage from './user_avatar_image.vue';
-import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
@@ -28,7 +27,7 @@ export default {
userAvatarImage,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
linkHref: {
@@ -94,11 +93,14 @@ export default {
:size="imgSize"
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
- /><span
+ >
+ <slot></slot>
+ </user-avatar-image><span
v-if="shouldShowUsername"
- v-tooltip
+ v-gl-tooltip
:title="tooltipText"
:tooltip-placement="tooltipPlacement"
- >{{ username }}</span>
+ class="js-user-avatar-link-username"
+ >{{ username }}</span><slot name="avatar-badge"></slot>
</gl-link>
</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
index 67a1632269e..f9e3f3df0cc 100644
--- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
@@ -14,7 +14,14 @@ export default {
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
- this.updateContent({ scope: this.scope, page: Number(page).toString() });
+ const params = {
+ page: Number(page).toString(),
+ };
+
+ if (this.scope) {
+ params.scope = this.scope;
+ }
+ this.updateContent(params);
},
updateInternalState(parameters) {
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 702276780e9..7a95db5976d 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -148,10 +148,7 @@
.award-control-icon svg {
background: $award-emoji-positive-add-bg;
-
- path {
- fill: $award-emoji-positive-add-lines;
- }
+ fill: $award-emoji-positive-add-lines;
}
.award-control-icon-neutral {
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index f26b1fddae5..43b7c26b272 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -348,6 +348,7 @@
@include media-breakpoint-down(xs) {
width: 100%;
+ margin: $btn-side-margin 0;
}
}
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index c4296c7a88a..219fd99b097 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -207,6 +207,10 @@
@include btn-with-margin;
}
+ &.btn-icon {
+ color: $gl-gray-700;
+ }
+
.fa-caret-down,
.fa-chevron-down {
margin-left: 5px;
@@ -218,6 +222,25 @@
}
}
+ &.btn-text-field {
+ width: 100%;
+ text-align: left;
+ padding: 6px 16px;
+ border-color: $border-color;
+ color: $gray-darkest;
+ background-color: $gray-light;
+
+ &:hover,
+ &:active,
+ &:focus {
+ cursor: text;
+ box-shadow: none;
+ border-color: lighten($blue-300, 20%);
+ color: $gray-darkest;
+ background-color: $gray-light;
+ }
+ }
+
&.dot-highlight::after {
content: '';
background-color: $blue-500;
@@ -335,25 +358,6 @@
}
}
-.btn-text-field {
- width: 100%;
- text-align: left;
- padding: 6px 16px;
- border-color: $border-color;
- color: $gray-darkest;
- background-color: $gray-light;
-
- &:hover,
- &:active,
- &:focus {
- cursor: text;
- box-shadow: none;
- border-color: lighten($blue-300, 20%);
- color: $gray-darkest;
- background-color: $gray-light;
- }
-}
-
.btn-build {
margin-left: 10px;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index fa753b13e5f..626c8f92d1d 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -33,6 +33,11 @@
color: $brand-danger;
}
+.text-danger-muted,
+.text-danger-muted:hover {
+ color: $red-300;
+}
+
.text-warning,
.text-warning:hover {
color: $brand-warning;
@@ -345,6 +350,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
+.prepend-top-4 { margin-top: $gl-padding-4; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
@@ -365,6 +371,7 @@ img.emoji {
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-4 { margin-bottom: $gl-padding-4; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; }
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 2e7f25d975e..6f103e4e89a 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -322,15 +322,15 @@
width: $contextual-sidebar-width - 1px;
transition: width $sidebar-transition-duration;
position: fixed;
+ height: $toggle-sidebar-height;
bottom: 0;
- padding: $gl-padding;
+ padding: 0 $gl-padding;
background-color: $gray-light;
border: 0;
border-top: 1px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
- line-height: 1;
svg {
margin-right: 8px;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cdfad30e7ca..dca89981d81 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -158,7 +158,7 @@
color: $gl-text-color;
outline: 0;
- // make sure the text color is not overriden
+ // make sure the text color is not overridden
&.text-danger {
color: $brand-danger;
}
@@ -184,7 +184,7 @@
text-align: left;
width: 100%;
- // make sure the text color is not overriden
+ // make sure the text color is not overridden
&.text-danger {
color: $brand-danger;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 53f198b47c6..6bdcb20210b 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -36,7 +36,6 @@
text-align: left;
padding: 10px $gl-padding;
word-wrap: break-word;
- border-radius: $border-radius-default $border-radius-default 0 0;
&.file-title-clear {
padding-left: 0;
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 1e93bf2b751..a20920e2503 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -39,7 +39,7 @@
svg {
fill: currentColor;
- $svg-sizes: 8 10 12 16 18 24 32 48 72;
+ $svg-sizes: 8 10 12 14 16 18 24 32 48 72;
@each $svg-size in $svg-sizes {
&.s#{$svg-size} {
@include svg-size(#{$svg-size}px);
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index c030d75f5a4..9837b1a6bd0 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -291,7 +291,7 @@
/*
* Mixin that handles the position of the controls placed on the top bar
*/
-@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size) {
+@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: 'block', $svg-top: '2px') {
display: flex;
font-size: $control-font-size;
justify-content: $flex-direction;
@@ -304,8 +304,9 @@
svg {
width: 15px;
height: 15px;
- display: block;
+ display: $svg-display;
fill: $gl-text-color;
+ top: $svg-top;
}
.controllers-buttons {
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index f47dfe1b563..de9e7c37695 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -1,6 +1,6 @@
// For tabbed navigation links, scrolling tabs, etc. For all top/main navigation,
// please check nav.scss
-.nav-links {
+.nav-links:not(.quick-links) {
display: flex;
padding: 0;
margin: 0;
@@ -106,7 +106,7 @@
display: inline-block;
float: right;
text-align: right;
- padding: 11px 0;
+ padding: $gl-padding-8 0;
margin-bottom: 0;
> .btn,
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 381c0290d32..7f0edd88dfb 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -243,6 +243,8 @@
.user-result {
min-height: 24px;
+ display: flex;
+ align-items: center;
.user-image {
float: left;
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 339388392df..6954e6599b1 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -147,3 +147,9 @@ table {
}
}
}
+
+.top-area + .content-list {
+ th {
+ border-top: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index dfb145debe7..4a311da1675 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -1,7 +1,7 @@
.timeline {
- @include basic-list;
margin: 0;
padding: 0;
+ list-style: none;
&::before {
@include notes-media('max', map-get($grid-breakpoints, sm)) {
@@ -26,10 +26,8 @@
}
.timeline-entry {
- border-color: $white-normal;
color: $gl-text-color;
- border-bottom: 1px solid $border-white-light;
- background: $white-light;
+ background-color: $white-light;
.timeline-entry-inner {
position: relative;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ad66a0365ed..f4540146a25 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -10,6 +10,7 @@ $sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
+$toggle-sidebar-height: 48px;
/*
* Color schema
@@ -194,6 +195,7 @@ $well-light-text-color: #5b6169;
* Text
*/
$gl-font-size: 14px;
+$gl-font-size-xs: 11px;
$gl-font-size-small: 12px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
@@ -268,6 +270,7 @@ $flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
$project-title-row-height: 24px;
+$gl-line-height: 16px;
/*
* Common component specific colors
@@ -438,7 +441,7 @@ $ci-skipped-color: #888;
* Boards
*/
$issue-boards-font-size: 14px;
-$issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
+$issue-boards-card-shadow: rgba(0, 0, 0, 0.1);
/*
The following heights are used in boards.scss and are used for calculation of the board height.
They probably should be derived in a smarter way.
@@ -638,3 +641,8 @@ Modals
$modal-body-height: 134px;
$priority-label-empty-state-width: 114px;
+
+/*
+Issues Analytics
+*/
+$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 54fbd40cece..c6074eb9df4 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -90,20 +90,14 @@
}
.with-performance-bar & {
- height: calc(
- 100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height});
@include media-breakpoint-only(sm) {
- height: calc(
- 100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height});
}
@include media-breakpoint-up(md) {
- height: calc(
- 100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height});
}
}
}
@@ -271,7 +265,7 @@
height: 100%;
width: 100%;
margin-bottom: 0;
- padding: 5px;
+ padding: $gl-padding-4;
list-style: none;
overflow-y: auto;
overflow-x: hidden;
@@ -284,14 +278,16 @@
.board-card {
position: relative;
- padding: 11px 10px 11px $gl-padding;
+ padding: $gl-padding;
background: $white-light;
border-radius: $border-radius-default;
+ border: 1px solid $theme-gray-200;
box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
+ line-height: $gl-padding;
&:not(:last-child) {
- margin-bottom: 5px;
+ margin-bottom: $gl-padding-8;
}
&.is-active,
@@ -302,113 +298,120 @@
.badge {
border: 0;
outline: 0;
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ @include media-breakpoint-down(lg) {
+ font-size: $gl-font-size-xs;
+ padding-left: $gl-padding-4;
+ padding-right: $gl-padding-4;
+ font-weight: $gl-font-weight-bold;
+ }
+ }
+
+ svg {
+ vertical-align: top;
}
.confidential-icon {
- vertical-align: text-top;
- margin-right: 5px;
+ color: $orange-600;
+ cursor: help;
+ }
+
+ @include media-breakpoint-down(md) {
+ padding: $gl-padding-8;
}
}
.board-card-title {
@include overflow-break-word();
- margin: 0 30px 0 0;
font-size: 1em;
- line-height: inherit;
a {
color: $gl-text-color;
- margin-right: 2px;
+ }
+
+ @include media-breakpoint-down(md) {
+ font-size: $label-font-size;
}
}
.board-card-header {
display: flex;
- min-height: 20px;
-
- .board-card-assignee {
- display: flex;
- justify-content: flex-end;
- position: absolute;
- right: 15px;
- height: 20px;
- width: 20px;
+}
- .avatar-counter {
- display: none;
- vertical-align: middle;
- min-width: 20px;
- line-height: 19px;
- height: 20px;
- padding-left: 2px;
- padding-right: 2px;
- border-radius: 2em;
- }
+.board-card-assignee {
+ display: flex;
+ margin-top: -$gl-padding-4;
+ margin-bottom: -$gl-padding-4;
+
+ .avatar-counter {
+ vertical-align: middle;
+ line-height: $gl-padding-24;
+ min-width: $gl-padding-24;
+ height: $gl-padding-24;
+ border-radius: $gl-padding-24;
+ background-color: $gl-text-color-tertiary;
+ font-size: $gl-font-size-xs;
+ cursor: help;
+ font-weight: $gl-font-weight-bold;
+ margin-left: -$gl-padding-4;
+ border: 0;
+ padding: 0 $gl-padding-4;
- img {
- vertical-align: top;
+ @include media-breakpoint-down(md) {
+ min-width: auto;
+ height: $gl-padding;
+ border-radius: $gl-padding;
+ line-height: $gl-padding;
}
+ }
- a {
- position: relative;
- margin-left: -15px;
- }
+ img {
+ vertical-align: top;
+ }
- a:nth-child(1) {
- z-index: 3;
- }
+ .user-avatar-link:not(:only-child) {
+ margin-left: -$gl-padding-4;
- a:nth-child(2) {
+ &:nth-of-type(1) {
z-index: 2;
}
- a:nth-child(3) {
+ &:nth-of-type(2) {
z-index: 1;
}
+ }
- a:nth-child(4) {
- display: none;
- }
-
- &:hover {
- .avatar-counter {
- display: inline-block;
- }
-
- a {
- position: static;
- background-color: $white-light;
- transition: background-color 0s;
- margin-left: auto;
-
- &:nth-child(4) {
- display: block;
- }
+ .avatar {
+ margin: 0;
- &:first-child:not(:only-child) {
- box-shadow: -10px 0 10px 1px $white-light;
- }
- }
+ @include media-breakpoint-down(md) {
+ width: $gl-padding;
+ height: $gl-padding;
}
}
- .avatar {
- margin: 0;
+ @include media-breakpoint-down(md) {
+ margin-top: 0;
+ margin-bottom: 0;
}
}
-.board-card-footer {
- margin: 0 0 5px;
+.board-card-number {
+ font-size: $gl-font-size-xs;
+ color: $gl-text-color-secondary;
+ overflow: hidden;
- .badge {
- margin-top: 5px;
- margin-right: 6px;
+ @include media-breakpoint-up(md) {
+ font-size: $label-font-size;
}
}
-.board-card-number {
- font-size: 12px;
- color: $gl-text-color-secondary;
+.board-card-number-container {
+ overflow: hidden;
}
.issue-boards-search {
@@ -474,8 +477,7 @@
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
- transition: width $sidebar-transition-duration,
- padding $sidebar-transition-duration;
+ transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
}
&.boards-sidebar-slide-enter,
@@ -650,3 +652,36 @@
}
}
}
+
+.board-card-info {
+ color: $gl-text-color-secondary;
+ white-space: nowrap;
+ margin-right: $gl-padding-8;
+
+ &:not(.board-card-weight) {
+ cursor: help;
+ }
+
+ &.board-card-weight {
+ color: $gl-text-color;
+ cursor: pointer;
+
+ &:hover {
+ color: initial;
+ text-decoration: underline;
+ }
+ }
+
+ .board-card-info-icon {
+ color: $theme-gray-600;
+ margin-right: $gl-padding-4;
+ }
+
+ @include media-breakpoint-down(md) {
+ font-size: $label-font-size;
+ }
+}
+
+.board-issue-path.js-show-tooltip {
+ cursor: help;
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 31b258e56dd..81cb519883b 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -55,9 +55,29 @@
@include build-trace();
}
+ .archived-sticky {
+ top: $header-height;
+ border-radius: 2px 2px 0 0;
+ color: $orange-600;
+ background-color: $orange-100;
+ border: 1px solid $border-gray-normal;
+ border-bottom: 0;
+ padding: 3px 12px;
+ margin: auto;
+ align-items: center;
+
+ .with-performance-bar & {
+ top: $header-height + $performance-bar-height;
+ }
+ }
+
.top-bar {
@include build-trace-top-bar(35px);
+ &.has-archived-block {
+ top: $header-height + $performance-bar-height + 28px;
+ }
+
&.affix {
top: $header-height;
@@ -94,7 +114,7 @@
}
.controllers {
- @include build-controllers(15px, center, false, 0);
+ @include build-controllers(15px, center, false, 0, inline, 0);
}
}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 71a3fd544f2..ad12cd101b6 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -25,6 +25,12 @@
.cluster-application-row {
border-bottom: 1px solid $border-color;
padding: $gl-padding;
+
+ &:last-child {
+ border-bottom: 0;
+ border-bottom-left-radius: calc(#{$border-radius-default} - 1px);
+ border-bottom-right-radius: calc(#{$border-radius-default} - 1px);
+ }
}
}
@@ -73,6 +79,10 @@
padding: $gl-padding-top $gl-padding;
}
+ .card {
+ margin-bottom: $gl-vert-padding;
+ }
+
.empty-state .svg-content img {
width: 145px;
}
@@ -80,6 +90,31 @@
.top-area .nav-controls > .btn.btn-add-cluster {
margin-right: 0;
}
+
+ .clusters-table {
+ background-color: $gray-light;
+ padding: $gl-padding-8;
+ }
+
+ .badge-light {
+ background-color: $white-normal;
+ }
+
+ .gl-responsive-table-row {
+ padding: $gl-padding;
+ border: 0;
+
+ &.table-row-header {
+ background-color: none;
+ border: 0;
+ font-weight: bold;
+ color: $gl-gray-500;
+ }
+ }
+}
+
+.cluster-warning {
+ @include alert-variant(theme-color-level('warning', $alert-bg-level), theme-color-level('warning', $alert-border-level), theme-color-level('warning', $alert-color-level));
}
.gcp-signup-offer {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 52c91266ff4..6d998fa1e07 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -59,6 +59,7 @@
margin: 0;
padding: 0;
table-layout: fixed;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
.diff-line-num {
width: 50px;
@@ -421,21 +422,13 @@
.diff-file-container {
.frame.deleted {
- border: 0;
+ border: 1px solid $deleted;
background-color: inherit;
-
- .image_file img {
- border: 1px solid $deleted;
- }
}
.frame.added {
- border: 0;
+ border: 1px solid $added;
background-color: inherit;
-
- .image_file img {
- border: 1px solid $added;
- }
}
.swipe.view,
@@ -481,6 +474,11 @@
bottom: -25px;
}
}
+
+ .discussion-notes .discussion-notes {
+ margin-left: 0;
+ border-left: 0;
+ }
}
.file-content .diff-file {
@@ -804,7 +802,7 @@
// double jagged line divider
.discussion-notes + .discussion-notes::before,
- .discussion-notes + .discussion-form::before {
+ .diff-file-discussions + .discussion-form::before {
content: '';
position: relative;
display: block;
@@ -844,6 +842,13 @@
background-repeat: repeat;
}
+ .diff-file-discussions + .discussion-form::before {
+ width: auto;
+ margin-left: -16px;
+ margin-right: -16px;
+ margin-bottom: 16px;
+ }
+
.notes {
position: relative;
}
@@ -855,7 +860,7 @@
}
.diff-file .note-container > .new-note,
-.note-container .discussion-notes {
+.note-container .discussion-notes.diff-discussions {
margin-left: 100px;
border-left: 1px solid $white-normal;
}
@@ -870,11 +875,13 @@
}
}
-.files:not([data-can-create-note]) .frame {
+.files:not([data-can-create-note="true"]) .frame {
cursor: auto;
}
-.frame.click-to-comment {
+.frame,
+.frame.click-to-comment,
+.btn-transparent.image-diff-overlay-add-comment {
position: relative;
cursor: image-url('illustrations/image_comment_light_cursor.svg')
$image-comment-cursor-left-offset $image-comment-cursor-top-offset,
@@ -910,6 +917,7 @@
.frame .badge.badge-pill,
.image-diff-avatar-link .badge.badge-pill,
+.user-avatar-link .badge.badge-pill,
.notes > .badge.badge-pill {
position: absolute;
background-color: $blue-400;
@@ -944,7 +952,8 @@
}
}
-.image-diff-avatar-link {
+.image-diff-avatar-link,
+.user-avatar-link {
position: relative;
.badge.badge-pill,
@@ -1073,3 +1082,14 @@
top: 0;
}
}
+
+.image-diff-overlay,
+.image-diff-overlay-add-comment {
+ top: 0;
+ left: 0;
+
+ &:active,
+ &:focus {
+ outline: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 19a36061c45..347fcad771a 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -44,11 +44,6 @@
margin: 0;
}
- .icon-play {
- height: 13px;
- width: 12px;
- }
-
.external-url,
.dropdown-new {
color: $gl-text-color-secondary;
@@ -366,7 +361,7 @@
}
.arrow-shadow {
- content: "";
+ content: '';
position: absolute;
width: 7px;
height: 7px;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index a91d44805ee..618f23d81b1 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -4,41 +4,29 @@
*/
.event-item {
font-size: $gl-font-size;
- padding: $gl-padding-top 0 $gl-padding-top 40px;
+ padding: $gl-padding 0 $gl-padding 56px;
border-bottom: 1px solid $white-normal;
- color: $gl-text-color;
+ color: $gl-text-color-secondary;
position: relative;
-
- &.event-inline {
- .system-note-image {
- top: 20px;
- }
-
- .user-avatar {
- top: 14px;
- }
-
- .event-title,
- .event-item-timestamp {
- line-height: 40px;
- }
- }
-
- a {
- color: $gl-text-color;
- }
+ line-height: $gl-line-height;
.system-note-image {
position: absolute;
left: 0;
- top: 14px;
svg {
- width: 20px;
- height: 20px;
fill: $gl-text-color-secondary;
}
+ }
+
+ .system-note-image-inline {
+ svg {
+ fill: $gl-text-color-secondary;
+ }
+ }
+ .system-note-image,
+ .system-note-image-inline {
&.opened-icon,
&.created-icon {
svg {
@@ -53,16 +41,35 @@
&.accepted-icon svg {
fill: $blue-300;
}
+
+ &.commented-on-icon svg {
+ fill: $blue-600;
+ }
+ }
+
+ .event-user-info {
+ margin-bottom: $gl-padding-8;
+
+ .author_name {
+ a {
+ color: $gl-text-color;
+ font-weight: $gl-font-weight-bold;
+ }
+ }
}
.event-title {
- @include str-truncated(calc(100% - 174px));
- font-weight: $gl-font-weight-bold;
- color: $gl-text-color;
+ .event-type {
+ &::first-letter {
+ text-transform: capitalize;
+ }
+ }
}
.event-body {
+ margin-top: $gl-padding-8;
margin-right: 174px;
+ color: $gl-text-color;
.event-note {
word-wrap: break-word;
@@ -92,7 +99,7 @@
}
.note-image-attach {
- margin-top: 4px;
+ margin-top: $gl-padding-4;
margin-left: 0;
max-width: 200px;
float: none;
@@ -107,7 +114,6 @@
color: $gl-gray-500;
float: left;
font-size: $gl-font-size;
- line-height: 16px;
margin-right: 5px;
}
}
@@ -127,7 +133,9 @@
}
}
- &:last-child { border: 0; }
+ &:last-child {
+ border: 0;
+ }
.event_commits {
li {
@@ -154,7 +162,6 @@
.event-item-timestamp {
float: right;
- line-height: 22px;
}
}
@@ -177,10 +184,8 @@
.event-item {
padding-left: 0;
- &.event-inline {
- .event-title {
- line-height: 20px;
- }
+ .event-user-info {
+ margin-bottom: $gl-padding-4;
}
.event-title {
@@ -194,7 +199,8 @@
}
.event-body {
- margin: 0;
+ margin-top: $gl-padding-4;
+ margin-right: 0;
padding-left: 0;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c60bb360a03..855d73a9939 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -239,6 +239,7 @@
.discussion-reply-holder {
background-color: $white-light;
padding: 10px 16px;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
&.is-replying {
padding-bottom: $gl-padding;
@@ -247,10 +248,15 @@
}
.discussion-with-resolve-btn {
+ @include media-breakpoint-up(sm) {
+ display: flex;
+ }
+
+
.discussion-actions {
display: table;
- .btn-default path {
+ svg {
fill: $gray-darkest;
}
@@ -270,6 +276,12 @@
.btn {
width: 100%;
}
+
+ .btn-text-field {
+ @include media-breakpoint-down(xs) {
+ margin-bottom: $gl-padding-8;
+ }
+ }
}
.discussion-notes-count {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index be535ade0a6..c57c1eee350 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -1,26 +1,135 @@
-/**
- * Notes
- */
+$system-note-icon-size: 32px;
+$system-note-svg-size: 16px;
+$note-form-margin-left: 70px;
-@-webkit-keyframes targe3-note {
- from {
- background: $note-targe3-outside;
+@mixin vertical-line($left) {
+ &::before {
+ content: '';
+ border-left: 2px solid $theme-gray-100;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: $left;
}
+}
- 50% {
- background: $note-targe3-inside;
- }
+.note-wrapper {
+ padding: $gl-padding;
+}
+
+.issuable-discussion {
+ .notes.timeline > .timeline-entry {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ margin: $gl-padding 0;
+
+ &.system-note,
+ &.note-form {
+ border: 0;
+ }
+
+ &.note-form {
+ margin-left: 0;
- to {
- background: $note-targe3-outside;
+ @include notes-media('min', map-get($grid-breakpoints, md)) {
+ margin-left: $note-form-margin-left;
+ }
+
+ .timeline-icon {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
+ margin-left: -$note-icon-gutter-width;
+ }
+ }
+
+ .timeline-content {
+ margin-left: 0;
+ }
+ }
+
+ .notes_content {
+ border: 0;
+ border-top: 1px solid $border-color;
+ }
}
}
-ul.notes {
+.main-notes-list {
+ @include vertical-line(39px);
+}
+
+.notes {
display: block;
list-style: none;
margin: 0;
padding: 0;
+ position: relative;
+
+ > .note-discussion {
+ .card {
+ border: 0;
+ }
+
+ li.note {
+ border-bottom: 1px solid $border-color;
+
+ &:first-child {
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ }
+ }
+ }
+
+ .replies-toggle {
+ background-color: $gray-light;
+ padding: $gl-padding-8 $gl-padding;
+
+ .collapse-replies-btn:hover {
+ color: $blue-600;
+ }
+
+ &.expanded {
+ border-bottom: 1px solid $border-color;
+
+ span {
+ cursor: pointer;
+ }
+
+ svg {
+ position: relative;
+ top: 3px;
+ }
+ }
+
+ &.collapsed {
+ color: $gl-text-color-secondary;
+
+ svg {
+ float: left;
+ position: relative;
+ top: $gl-padding-4;
+ margin-right: $gl-padding-8;
+ cursor: pointer;
+ }
+
+ img {
+ margin: -2px 4px 0 0;
+ }
+
+ .author-link {
+ color: $gl-text-color;
+ }
+ }
+
+ .user-avatar-link {
+ &:last-child img {
+ margin-right: $gl-padding-8;
+ }
+ }
+
+ .btn-link {
+ border: 0;
+ vertical-align: baseline;
+ }
+ }
.note-created-ago,
.note-updated-at {
@@ -28,8 +137,6 @@ ul.notes {
}
.discussion-body {
- padding-top: 8px;
-
.card {
margin-bottom: 0;
}
@@ -46,21 +153,10 @@ ul.notes {
}
> li {
- // .timeline-entry
- padding: 0;
display: block;
position: relative;
border-bottom: 0;
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- padding-left: $note-icon-gutter-width;
- }
-
- .timeline-entry-inner {
- padding: $gl-padding $gl-btn-padding;
- border-bottom: 1px solid $white-normal;
- }
-
&:target,
&.target {
border-bottom: 1px solid $white-normal;
@@ -75,23 +171,10 @@ ul.notes {
}
}
- .timeline-icon {
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: -$note-icon-gutter-width;
- }
- }
-
- .timeline-content {
- margin-left: $note-icon-gutter-width;
-
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: 0;
- }
- }
-
&.being-posted {
pointer-events: none;
opacity: 0.5;
+ padding: $gl-padding;
.dummy-avatar {
background-color: $gl-gray-200;
@@ -104,12 +187,6 @@ ul.notes {
}
}
- &.note-discussion {
- .timeline-entry-inner {
- padding: $gl-padding 10px;
- }
- }
-
.editing-spinner {
display: none;
}
@@ -191,8 +268,9 @@ ul.notes {
}
.system-note {
- font-size: 14px;
- clear: both;
+ padding: 6px $gl-padding-24;
+ margin: $gl-padding-24 0;
+ background-color: transparent;
.note-header-info {
padding-bottom: 0;
@@ -225,17 +303,21 @@ ul.notes {
.timeline-icon {
float: left;
-
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: 0;
- width: auto;
- }
+ display: flex;
+ align-items: center;
+ background-color: $white-light;
+ width: $system-note-icon-size;
+ height: $system-note-icon-size;
+ border: 1px solid $border-color;
+ border-radius: $system-note-icon-size;
+ margin: -6px $gl-padding 0 0;
svg {
- width: 16px;
- height: 16px;
+ width: $system-note-svg-size;
+ height: $system-note-svg-size;
fill: $gray-darkest;
- margin-top: 2px;
+ display: block;
+ margin: 0 auto;
}
}
@@ -302,10 +384,17 @@ ul.notes {
.discussion-body .diff-file {
.file-title {
cursor: default;
+ line-height: 42px;
+ padding: 0 $gl-padding;
+ border-top: 1px solid $border-color;
&:hover {
background-color: $gray-light;
}
+
+ .btn-clipboard {
+ top: 10px;
+ }
}
.line_content {
@@ -320,6 +409,23 @@ ul.notes {
}
}
+ .discussion-notes {
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
+ margin-top: 20px;
+ }
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ margin-bottom: 20px;
+ }
+
+ .system-note {
+ margin: 0;
+ padding: $gl-padding;
+ }
+ }
+
// Merge request notes in diffs
// Diff is inline
.notes_content .note-header .note-headline-light {
@@ -335,7 +441,6 @@ ul.notes {
border-left: 0;
&.notes_content {
- background-color: $gray-light;
border-width: 1px 0;
padding: 0;
vertical-align: top;
@@ -349,18 +454,6 @@ ul.notes {
}
}
- .discussion-notes {
- &:not(:first-child) {
- border-top: 1px solid $white-normal;
- margin-top: 20px;
- }
-
- &:not(:last-child) {
- border-bottom: 1px solid $white-normal;
- margin-bottom: 20px;
- }
- }
-
.notes {
background-color: $white-light;
}
@@ -374,6 +467,30 @@ ul.notes {
}
}
+.diffs {
+ .discussion-notes {
+ margin-left: 0;
+ border-left: 0;
+
+ .notes {
+ position: relative;
+ @include vertical-line(52px);
+ }
+ }
+
+ .note-wrapper {
+ margin: $gl-padding;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ }
+
+ .discussion-reply-holder {
+ border-radius: 0 0 $border-radius-default $border-radius-default;
+ border-top: 1px solid $border-color;
+ position: relative;
+ }
+}
+
.discussion-header,
.note-header-info {
a {
@@ -399,7 +516,17 @@ ul.notes {
}
.discussion-header {
- font-size: 14px;
+ min-height: 72px;
+
+ .note-header-info {
+ padding-bottom: 0;
+ }
+}
+
+.unresolved {
+ .note-header-info {
+ margin-top: $gl-padding-8;
+ }
}
.note-header {
@@ -409,7 +536,7 @@ ul.notes {
.note-header-info {
min-width: 0;
- padding-bottom: 8px;
+ padding-bottom: $gl-padding-8;
&.discussion {
padding-bottom: 0;
@@ -468,12 +595,20 @@ ul.notes {
.discussion-actions {
float: right;
- margin-left: 10px;
color: $gray-darkest;
+ @include media-breakpoint-down(xs) {
+ width: 100%;
+ margin: $gl-padding-8 0;
+ }
+
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
+
+ svg {
+ height: 15px;
+ }
}
.note-actions {
@@ -585,19 +720,6 @@ ul.notes {
z-index: 10;
}
-.discussion-body,
-.diff-file {
- .notes .note {
- border-bottom: 1px solid $white-normal;
-
- .timeline-entry-inner {
- padding-left: $gl-padding;
- padding-right: $gl-padding;
- border-bottom: 0;
- }
- }
-}
-
.disabled-comment {
background-color: $gray-light;
border-radius: $border-radius-base;
@@ -634,7 +756,7 @@ ul.notes {
}
.btn {
- svg path {
+ svg {
fill: $gray-darkest;
}
@@ -659,7 +781,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 5px 10px 6px;
+ padding: 6px 10px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
diff --git a/app/assets/stylesheets/pages/pages.scss b/app/assets/stylesheets/pages/pages.scss
index fb42dee66d2..374227fe16a 100644
--- a/app/assets/stylesheets/pages/pages.scss
+++ b/app/assets/stylesheets/pages/pages.scss
@@ -1,7 +1,5 @@
.pages-domain-list {
&-item {
- position: relative;
- display: flex;
align-items: center;
.domain-status {
@@ -44,8 +42,9 @@
}
:first-child {
- border-bottom-left-radius: $border-radius-default;
- border-top-left-radius: $border-radius-default;
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0;
+ line-height: $gl-line-height;
}
:not(:first-child) {
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index 86e70955389..617b3db2fae 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -39,10 +39,6 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-
- svg {
- vertical-align: middle;
- }
}
.next-run-cell {
@@ -52,6 +48,10 @@
a {
color: $text-color;
}
+
+ svg {
+ vertical-align: middle;
+ }
}
.pipeline-schedules-user-callout {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index f084adaf5d3..1d691d1d8b8 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -240,6 +240,12 @@
left: 0;
}
+ .activities-block {
+ .event-item {
+ padding-left: 40px;
+ }
+ }
+
@include media-breakpoint-down(xs) {
.cover-block {
padding-top: 20px;
@@ -267,6 +273,12 @@
margin-right: 0;
}
}
+
+ .activities-block {
+ .event-item {
+ padding-left: 0;
+ }
+ }
}
}