summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
commit859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch)
treed7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /app/assets
parent446d496a6d000c73a304be52587cd9bbc7493136 (diff)
downloadgitlab-ce-859a6fb938bb9ee2a317c46dfa4fcc1af49608f0.tar.gz
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/images/auth_buttons/openid_64.pngbin0 -> 1918 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/create-0.pngbin0 -> 10275 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/create-1.pngbin0 -> 39565 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/create-2.pngbin0 -> 15793 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.pngbin0 -> 42439 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/team-0.pngbin0 -> 42448 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/team-1.pngbin0 -> 62019 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/team-2.pngbin0 -> 54468 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/trial-0.pngbin0 -> 50665 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/trial-1.pngbin0 -> 8676 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/trial-2.pngbin0 -> 47411 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/verify-0.pngbin0 -> 15366 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/verify-1.pngbin0 -> 60722 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/verify-2.pngbin0 -> 57506 bytes
-rw-r--r--app/assets/javascripts/actioncable_connection_monitor.js142
-rw-r--r--app/assets/javascripts/actioncable_consumer.js9
-rw-r--r--app/assets/javascripts/activities.js2
-rw-r--r--app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue7
-rw-r--r--app/assets/javascripts/add_context_commits_modal/index.js2
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/actions.js4
-rw-r--r--app/assets/javascripts/admin/statistics_panel/components/app.vue6
-rw-r--r--app/assets/javascripts/admin/statistics_panel/index.js2
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/actions.js2
-rw-r--r--app/assets/javascripts/admin/users/components/actions/activate.vue44
-rw-r--r--app/assets/javascripts/admin/users/components/actions/approve.vue21
-rw-r--r--app/assets/javascripts/admin/users/components/actions/block.vue53
-rw-r--r--app/assets/javascripts/admin/users/components/actions/deactivate.vue60
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete.vue25
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue25
-rw-r--r--app/assets/javascripts/admin/users/components/actions/index.js21
-rw-r--r--app/assets/javascripts/admin/users/components/actions/reject.vue21
-rw-r--r--app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue43
-rw-r--r--app/assets/javascripts/admin/users/components/actions/unblock.vue44
-rw-r--r--app/assets/javascripts/admin/users/components/actions/unlock.vue42
-rw-r--r--app/assets/javascripts/admin/users/components/usage_ping_disabled.vue (renamed from app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue)0
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue119
-rw-r--r--app/assets/javascripts/admin/users/components/user_avatar.vue30
-rw-r--r--app/assets/javascripts/admin/users/components/user_date.vue29
-rw-r--r--app/assets/javascripts/admin/users/components/users_table.vue18
-rw-r--r--app/assets/javascripts/admin/users/constants.js21
-rw-r--r--app/assets/javascripts/admin/users/index.js25
-rw-r--r--app/assets/javascripts/admin/users/tabs.js23
-rw-r--r--app/assets/javascripts/admin/users/utils.js7
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue18
-rw-r--r--app/assets/javascripts/alert_management/constants.js31
-rw-r--r--app/assets/javascripts/alert_management/list.js4
-rw-r--r--app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue187
-rw-r--r--app/assets/javascripts/alerts_service_settings/index.js39
-rw-r--r--app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue77
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue47
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue26
-rw-r--r--app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json112
-rw-r--r--app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json78
-rw-r--r--app/assets/javascripts/alerts_settings/graphql.js2
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql18
-rw-r--r--app/assets/javascripts/alerts_settings/index.js12
-rw-r--r--app/assets/javascripts/alerts_settings/utils/mapping_transformations.js61
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/app.vue6
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue6
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue8
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue4
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue4
-rw-r--r--app/assets/javascripts/api.js60
-rw-r--r--app/assets/javascripts/api/user_api.js4
-rw-r--r--app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_application_setting.query.graphql5
-rw-r--r--app/assets/javascripts/artifacts_settings/index.js2
-rw-r--r--app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue57
-rw-r--r--app/assets/javascripts/authentication/mount_2fa.js2
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue6
-rw-r--r--app/assets/javascripts/authentication/u2f/authenticate.js2
-rw-r--r--app/assets/javascripts/authentication/u2f/register.js2
-rw-r--r--app/assets/javascripts/awards_handler.js14
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue6
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue4
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue2
-rw-r--r--app/assets/javascripts/badges/store/actions.js2
-rw-r--r--app/assets/javascripts/badges/store/index.js2
-rw-r--r--app/assets/javascripts/badges/store/mutations.js2
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue11
-rw-r--r--app/assets/javascripts/batch_comments/components/drafts_count.vue2
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_dropdown.vue2
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_item.vue19
-rw-r--r--app/assets/javascripts/batch_comments/components/publish_button.vue2
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js28
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/index.js4
-rw-r--r--app/assets/javascripts/behaviors/autosize.js12
-rw-r--r--app/assets/javascripts/behaviors/collapse_sidebar_on_window_resize.js2
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js2
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js12
-rw-r--r--app/assets/javascripts/behaviors/index.js38
-rw-r--r--app/assets/javascripts/behaviors/markdown/editor_extensions.js63
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/bold.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/code.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/inline_html.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/italic.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/link.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/math.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/blockquote.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/heading.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/image.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/list_item.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/paragraph.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/playable.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/text.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js2
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js2
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/behaviors/secret_values.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js9
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js88
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue525
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js8
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js2
-rw-r--r--app/assets/javascripts/blob/3d_viewer/index.js4
-rw-r--r--app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js2
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js2
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js6
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue4
-rw-r--r--app/assets/javascripts/blob/components/blob_header_filepath.vue4
-rw-r--r--app/assets/javascripts/blob/components/constants.js2
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js8
-rw-r--r--app/assets/javascripts/blob/file_template_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/dockerfile_selector.js4
-rw-r--r--app/assets/javascripts/blob/template_selectors/gitignore_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/license_selector.js3
-rw-r--r--app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/type_selector.js2
-rw-r--r--app/assets/javascripts/blob/viewer/index.js6
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js6
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js10
-rw-r--r--app/assets/javascripts/boards/boards_util.js14
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_trigger.vue21
-rw-r--r--app/assets/javascripts/boards/components/board_assignee_dropdown.vue196
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout.vue26
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout_deprecated.vue102
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue18
-rw-r--r--app/assets/javascripts/boards/components/board_column_deprecated.vue16
-rw-r--r--app/assets/javascripts/boards/components/board_configuration_options.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue12
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue26
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue10
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue20
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue43
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_deprecated.vue41
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue_deprecated.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue19
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js67
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue38
-rw-r--r--app/assets/javascripts/boards/components/boards_selector_deprecated.vue357
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue10
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue6
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue4
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js2
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue8
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue2
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js41
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue2
-rw-r--r--app/assets/javascripts/boards/components/project_select_deprecated.vue4
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue20
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue62
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue8
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue86
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue2
-rw-r--r--app/assets/javascripts/boards/components/toggle_focus.vue52
-rw-r--r--app/assets/javascripts/boards/constants.js20
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js8
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js2
-rw-r--r--app/assets/javascripts/boards/graphql/project_milestones.query.graphql (renamed from app/assets/javascripts/boards/graphql/group_milestones.query.graphql)6
-rw-r--r--app/assets/javascripts/boards/index.js50
-rw-r--r--app/assets/javascripts/boards/models/issue.js6
-rw-r--r--app/assets/javascripts/boards/models/iteration.js9
-rw-r--r--app/assets/javascripts/boards/models/list.js11
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js20
-rw-r--r--app/assets/javascripts/boards/stores/actions.js105
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js22
-rw-r--r--app/assets/javascripts/boards/stores/getters.js10
-rw-r--r--app/assets/javascripts/boards/stores/index.js4
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js7
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js34
-rw-r--r--app/assets/javascripts/boards/stores/state.js4
-rw-r--r--app/assets/javascripts/boards/toggle_focus.js48
-rw-r--r--app/assets/javascripts/branches/components/divergence_graph.vue2
-rw-r--r--app/assets/javascripts/branches/divergence_graph.js2
-rw-r--r--app/assets/javascripts/build_artifacts.js4
-rw-r--r--app/assets/javascripts/captcha/captcha_modal.vue110
-rw-r--r--app/assets/javascripts/captcha/init_recaptcha_script.js48
-rw-r--r--app/assets/javascripts/ci_lint/components/ci_lint.vue2
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue2
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/index.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue4
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue4
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/store/actions.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutations.js2
-rw-r--r--app/assets/javascripts/clone_panel.js5
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js16
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue5
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue22
-rw-r--r--app/assets/javascripts/clusters/components/fluentd_output_settings.vue2
-rw-r--r--app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue6
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue4
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue88
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js2
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue6
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js8
-rw-r--r--app/assets/javascripts/clusters_list/store/index.js4
-rw-r--r--app/assets/javascripts/code_navigation/components/app.vue2
-rw-r--r--app/assets/javascripts/code_navigation/index.js2
-rw-r--r--app/assets/javascripts/code_navigation/store/actions.js2
-rw-r--r--app/assets/javascripts/code_navigation/store/index.js2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js28
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue19
-rw-r--r--app/assets/javascripts/commits.js2
-rw-r--r--app/assets/javascripts/compare_autocomplete.js8
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue4
-rw-r--r--app/assets/javascripts/contextual_sidebar.js4
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue10
-rw-r--r--app/assets/javascripts/contributors/stores/index.js6
-rw-r--r--app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue8
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue2
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue4
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue14
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js2
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/actions.js8
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/index.js10
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js4
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/index.js7
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/store/actions.js2
-rw-r--r--app/assets/javascripts/create_cluster/init_create_cluster.js2
-rw-r--r--app/assets/javascripts/create_cluster/store/cluster_dropdown/index.js2
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js10
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue4
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue6
-rw-r--r--app/assets/javascripts/cycle_analytics/components/banner.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js12
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js2
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue4
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue2
-rw-r--r--app/assets/javascripts/deploy_freeze/store/actions.js2
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue2
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue4
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue2
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js11
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js2
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue12
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue10
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_presentation.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue10
-rw-r--r--app/assets/javascripts/design_management/components/design_todo_button.vue4
-rw-r--r--app/assets/javascripts/design_management/components/image.vue2
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue23
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/index.vue4
-rw-r--r--app/assets/javascripts/design_management/graphql.js10
-rw-r--r--app/assets/javascripts/design_management/index.js2
-rw-r--r--app/assets/javascripts/design_management/mixins/all_designs.js2
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue24
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue47
-rw-r--r--app/assets/javascripts/design_management/router/routes.js2
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js2
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js8
-rw-r--r--app/assets/javascripts/design_management/utils/tracking.js2
-rw-r--r--app/assets/javascripts/diff.js6
-rw-r--r--app/assets/javascripts/diffs/components/app.vue50
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue12
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_comment_cell.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue24
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussion_reply.vue11
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue65
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue77
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_row.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue20
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue23
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js4
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue28
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue4
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue2
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue8
-rw-r--r--app/assets/javascripts/diffs/components/no_changes.vue2
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue4
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue6
-rw-r--r--app/assets/javascripts/diffs/components/settings_dropdown.vue4
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue2
-rw-r--r--app/assets/javascripts/diffs/i18n.js7
-rw-r--r--app/assets/javascripts/diffs/index.js7
-rw-r--r--app/assets/javascripts/diffs/store/actions.js46
-rw-r--r--app/assets/javascripts/diffs/store/getters.js19
-rw-r--r--app/assets/javascripts/diffs/store/getters_versions_dropdowns.js2
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js19
-rw-r--r--app/assets/javascripts/diffs/store/utils.js2
-rw-r--r--app/assets/javascripts/diffs/utils/diff_file.js6
-rw-r--r--app/assets/javascripts/diffs/utils/file_reviews.js25
-rw-r--r--app/assets/javascripts/diffs/utils/performance.js2
-rw-r--r--app/assets/javascripts/diffs/utils/suggestions.js28
-rw-r--r--app/assets/javascripts/diffs/utils/uuids.js2
-rw-r--r--app/assets/javascripts/dirty_submit/dirty_submit_form.js2
-rw-r--r--app/assets/javascripts/droplab/drop_down.js2
-rw-r--r--app/assets/javascripts/droplab/drop_lab.js4
-rw-r--r--app/assets/javascripts/dropzone_input.js10
-rw-r--r--app/assets/javascripts/due_date_select.js6
-rw-r--r--app/assets/javascripts/editor/constants.js5
-rw-r--r--app/assets/javascripts/editor/editor_lite.js221
-rw-r--r--app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js2
-rw-r--r--app/assets/javascripts/emoji/index.js256
-rw-r--r--app/assets/javascripts/environments/components/canary_ingress.vue2
-rw-r--r--app/assets/javascripts/environments/components/canary_update_modal.vue2
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue2
-rw-r--r--app/assets/javascripts/environments/components/container.vue29
-rw-r--r--app/assets/javascripts/environments/components/deploy_board.vue11
-rw-r--r--app/assets/javascripts/environments/components/enable_review_app_modal.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_delete.vue5
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue16
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue3
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue38
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue25
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js4
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue26
-rw-r--r--app/assets/javascripts/environments/index.js6
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js14
-rw-r--r--app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js (renamed from app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js)3
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js2
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue12
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue12
-rw-r--r--app/assets/javascripts/error_tracking/details.js4
-rw-r--r--app/assets/javascripts/error_tracking/list.js2
-rw-r--r--app/assets/javascripts/error_tracking/store/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/details/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/index.js10
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutations.js4
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/app.vue4
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue2
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/index.js2
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/mutations.js2
-rw-r--r--app/assets/javascripts/feature_flags/components/edit_feature_flag.vue4
-rw-r--r--app/assets/javascripts/feature_flags/components/environments_dropdown.vue4
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags.vue19
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags_tab.vue64
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags_table.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/form.vue35
-rw-r--r--app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue4
-rw-r--r--app/assets/javascripts/feature_flags/components/new_feature_flag.vue13
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/parameter_form_group.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategy.vue4
-rw-r--r--app/assets/javascripts/feature_flags/components/strategy_parameters.vue2
-rw-r--r--app/assets/javascripts/feature_flags/edit.js2
-rw-r--r--app/assets/javascripts/feature_flags/new.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/edit/actions.js4
-rw-r--r--app/assets/javascripts/feature_flags/store/edit/index.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/edit/mutations.js4
-rw-r--r--app/assets/javascripts/feature_flags/store/gitlab_user_list/index.js4
-rw-r--r--app/assets/javascripts/feature_flags/store/gitlab_user_list/mutations.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/index/actions.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/index/index.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/index/mutations.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/new/actions.js2
-rw-r--r--app/assets/javascripts/feature_flags/store/new/index.js2
-rw-r--r--app/assets/javascripts/feature_highlight/constants.js1
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js59
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js28
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_options.js12
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_popover.vue101
-rw-r--r--app/assets/javascripts/feature_highlight/index.js28
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js10
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_ajax_filter.js6
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js6
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js6
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_operator.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js26
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js2
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js2
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_root.js19
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_storage_keys.js2
-rw-r--r--app/assets/javascripts/filtered_search/services/recent_searches_service.js2
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js6
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue6
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue2
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue2
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue22
-rw-r--r--app/assets/javascripts/frequent_items/index.js2
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js4
-rw-r--r--app/assets/javascripts/frequent_items/utils.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js145
-rw-r--r--app/assets/javascripts/gl_form.js4
-rw-r--r--app/assets/javascripts/gpg_badges.js4
-rw-r--r--app/assets/javascripts/grafana_integration/components/grafana_integration.vue4
-rw-r--r--app/assets/javascripts/grafana_integration/index.js2
-rw-r--r--app/assets/javascripts/grafana_integration/store/actions.js4
-rw-r--r--app/assets/javascripts/grafana_integration/store/index.js2
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql2
-rw-r--r--app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql (renamed from app/assets/javascripts/graphql_shared/mutations/update_alert_status.mutation.graphql)2
-rw-r--r--app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql2
-rw-r--r--app/assets/javascripts/group.js4
-rw-r--r--app/assets/javascripts/group_label_subscription.js4
-rw-r--r--app/assets/javascripts/group_settings/components/shared_runners_form.vue2
-rw-r--r--app/assets/javascripts/groups/components/app.vue6
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue20
-rw-r--r--app/assets/javascripts/groups/components/groups.vue2
-rw-r--r--app/assets/javascripts/groups/components/invite_members_banner.vue2
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue2
-rw-r--r--app/assets/javascripts/groups/groups_filterable_list.js2
-rw-r--r--app/assets/javascripts/groups/index.js14
-rw-r--r--app/assets/javascripts/groups/members/constants.js4
-rw-r--r--app/assets/javascripts/groups/members/utils.js47
-rw-r--r--app/assets/javascripts/groups/transfer_dropdown.js2
-rw-r--r--app/assets/javascripts/groups_select.js4
-rw-r--r--app/assets/javascripts/header.js8
-rw-r--r--app/assets/javascripts/helpers/avatar_helper.js2
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue10
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue4
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue17
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue4
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue93
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue10
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue2
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue2
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue4
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide.vue20
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue8
-rw-r--r--app/assets/javascripts/ide/components/ide_sidebar_nav.vue5
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide_status_list.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue4
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue6
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue6
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown_button.vue2
-rw-r--r--app/assets/javascripts/ide/components/nav_form.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue7
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue2
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue6
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue9
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue12
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue3
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue18
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue4
-rw-r--r--app/assets/javascripts/ide/components/terminal/session.vue4
-rw-r--r--app/assets/javascripts/ide/components/terminal/terminal.vue4
-rw-r--r--app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue2
-rw-r--r--app/assets/javascripts/ide/constants.js11
-rw-r--r--app/assets/javascripts/ide/ide_router.js4
-rw-r--r--app/assets/javascripts/ide/index.js10
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js4
-rw-r--r--app/assets/javascripts/ide/lib/create_diff.js2
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js4
-rw-r--r--app/assets/javascripts/ide/lib/editor.js12
-rw-r--r--app/assets/javascripts/ide/lib/errors.js4
-rw-r--r--app/assets/javascripts/ide/lib/languages/index.js2
-rw-r--r--app/assets/javascripts/ide/lib/mirror.js2
-rw-r--r--app/assets/javascripts/ide/lib/themes/index.js6
-rw-r--r--app/assets/javascripts/ide/services/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions.js14
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js8
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js150
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js4
-rw-r--r--app/assets/javascripts/ide/stores/getters.js9
-rw-r--r--app/assets/javascripts/ide/stores/index.js16
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js18
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/constants.js9
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/editor/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/editor/setup.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/actions.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/router/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/messages.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal_sync/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js6
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/plugins/terminal_sync.js4
-rw-r--r--app/assets/javascripts/ide/stores/utils.js2
-rw-r--r--app/assets/javascripts/ide/utils.js4
-rw-r--r--app/assets/javascripts/image_diff/helpers/init_image_diff.js2
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js2
-rw-r--r--app/assets/javascripts/image_diff/replaced_image_diff.js2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue212
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js61
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql14
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js10
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js85
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js17
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue36
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue61
-rw-r--r--app/assets/javascripts/import_entities/import_projects/index.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js12
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/index.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/mutations.js2
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue20
-rw-r--r--app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue4
-rw-r--r--app/assets/javascripts/incidents_settings/incidents_settings_service.js2
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js2
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js8
-rw-r--r--app/assets/javascripts/init_labels.js2
-rw-r--r--app/assets/javascripts/integrations/edit/components/active_checkbox.vue2
-rw-r--r--app/assets/javascripts/integrations/edit/components/confirmation_modal.vue2
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue6
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue183
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue41
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue2
-rw-r--r--app/assets/javascripts/integrations/edit/components/override_dropdown.vue2
-rw-r--r--app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue2
-rw-r--r--app/assets/javascripts/integrations/edit/components/trigger_fields.vue4
-rw-r--r--app/assets/javascripts/integrations/edit/index.js22
-rw-r--r--app/assets/javascripts/integrations/edit/store/actions.js15
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutations.js9
-rw-r--r--app/assets/javascripts/integrations/edit/store/state.js3
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js55
-rw-r--r--app/assets/javascripts/invite_member/components/invite_member_modal.vue5
-rw-r--r--app/assets/javascripts/invite_member/components/invite_member_trigger.vue2
-rw-r--r--app/assets/javascripts/invite_member/init_invite_member_modal.js2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue9
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue4
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js4
-rw-r--r--app/assets/javascripts/issuable/components/issuable_by_email.vue169
-rw-r--r--app/assets/javascripts/issuable/init_issuable_by_email.js35
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js3
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js16
-rw-r--r--app/assets/javascripts/issuable_context.js4
-rw-r--r--app/assets/javascripts/issuable_create/components/issuable_form.vue3
-rw-r--r--app/assets/javascripts/issuable_form.js6
-rw-r--r--app/assets/javascripts/issuable_index.js28
-rw-r--r--app/assets/javascripts/issuable_init_bulk_update_sidebar.js2
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue34
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_list_root.vue9
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_tabs.vue7
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_body.vue2
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_description.vue2
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_edit_form.vue2
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_header.vue12
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_show_root.vue2
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_title.vue2
-rw-r--r--app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue2
-rw-r--r--app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql1
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/app.vue2
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/item.vue4
-rw-r--r--app/assets/javascripts/issuable_suggestions/index.js2
-rw-r--r--app/assets/javascripts/issue.js6
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue15
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue8
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue10
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue15
-rw-r--r--app/assets/javascripts/issue_show/components/header_actions.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue8
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/incident.js2
-rw-r--r--app/assets/javascripts/issue_show/issue.js1
-rw-r--r--app/assets/javascripts/issue_show/utils/parse_data.js2
-rw-r--r--app/assets/javascripts/issue_status_select.js2
-rw-r--r--app/assets/javascripts/issues_list/components/issuable.vue13
-rw-r--r--app/assets/javascripts/issues_list/components/issuables_list_app.vue18
-rw-r--r--app/assets/javascripts/issues_list/components/jira_issues_list_root.vue4
-rw-r--r--app/assets/javascripts/issues_list/index.js2
-rw-r--r--app/assets/javascripts/jira_connect/api.js20
-rw-r--r--app/assets/javascripts/jira_connect/components/app.vue66
-rw-r--r--app/assets/javascripts/jira_connect/components/groups_list.vue74
-rw-r--r--app/assets/javascripts/jira_connect/components/groups_list_item.vue47
-rw-r--r--app/assets/javascripts/jira_connect/index.js97
-rw-r--r--app/assets/javascripts/jira_connect/store/index.js5
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue27
-rw-r--r--app/assets/javascripts/jira_import/queries/search_project_members.query.graphql13
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue29
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue2
-rw-r--r--app/assets/javascripts/jobs/components/environments_block.vue6
-rw-r--r--app/assets/javascripts/jobs/components/erased_block.vue20
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue29
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue4
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue4
-rw-r--r--app/assets/javascripts/jobs/components/log/duration_badge.vue9
-rw-r--r--app/assets/javascripts/jobs/components/log/line_header.vue2
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue29
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue14
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_job_details_container.vue24
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue2
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue2
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue55
-rw-r--r--app/assets/javascripts/jobs/index.js2
-rw-r--r--app/assets/javascripts/jobs/store/actions.js10
-rw-r--r--app/assets/javascripts/jobs/store/getters.js10
-rw-r--r--app/assets/javascripts/jobs/store/index.js2
-rw-r--r--app/assets/javascripts/label_manager.js9
-rw-r--r--app/assets/javascripts/labels_select.js14
-rw-r--r--app/assets/javascripts/lib/graphql.js23
-rw-r--r--app/assets/javascripts/lib/utils/array_utility.js20
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/color_utils.js20
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js16
-rw-r--r--app/assets/javascripts/lib/utils/constants.js6
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js157
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/poll.js2
-rw-r--r--app/assets/javascripts/lib/utils/poll_until_complete.js2
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js12
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js2
-rw-r--r--app/assets/javascripts/lib/utils/unit_format/formatter_factory.js3
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js58
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js5
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue15
-rw-r--r--app/assets/javascripts/logs/components/log_advanced_filters.vue2
-rw-r--r--app/assets/javascripts/logs/components/log_simple_filters.vue4
-rw-r--r--app/assets/javascripts/logs/stores/actions.js4
-rw-r--r--app/assets/javascripts/logs/stores/mutations.js2
-rw-r--r--app/assets/javascripts/logs/stores/state.js2
-rw-r--r--app/assets/javascripts/main.js25
-rw-r--r--app/assets/javascripts/manual_ordering.js4
-rw-r--r--app/assets/javascripts/members.js4
-rw-r--r--app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue4
-rw-r--r--app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/leave_button.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue3
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue4
-rw-r--r--app/assets/javascripts/members/components/app.vue (renamed from app/assets/javascripts/groups/members/components/app.vue)10
-rw-r--r--app/assets/javascripts/members/components/avatars/user_avatar.vue7
-rw-r--r--app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue2
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue6
-rw-r--r--app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue4
-rw-r--r--app/assets/javascripts/members/components/modals/leave_modal.vue2
-rw-r--r--app/assets/javascripts/members/components/modals/remove_group_link_modal.vue3
-rw-r--r--app/assets/javascripts/members/components/table/member_action_buttons.vue6
-rw-r--r--app/assets/javascripts/members/components/table/member_avatar.vue4
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue20
-rw-r--r--app/assets/javascripts/members/components/table/members_table_cell.vue8
-rw-r--r--app/assets/javascripts/members/constants.js5
-rw-r--r--app/assets/javascripts/members/index.js (renamed from app/assets/javascripts/groups/members/index.js)8
-rw-r--r--app/assets/javascripts/members/store/actions.js6
-rw-r--r--app/assets/javascripts/members/store/index.js4
-rw-r--r--app/assets/javascripts/members/store/mutations.js18
-rw-r--r--app/assets/javascripts/members/utils.js62
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js11
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js5
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js3
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js2
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js8
-rw-r--r--app/assets/javascripts/merge_request.js8
-rw-r--r--app/assets/javascripts/merge_request/components/status_box.vue6
-rw-r--r--app/assets/javascripts/merge_request_tabs.js76
-rw-r--r--app/assets/javascripts/milestone.js4
-rw-r--r--app/assets/javascripts/milestone_select.js29
-rw-r--r--app/assets/javascripts/milestones/components/milestone_results_section.vue7
-rw-r--r--app/assets/javascripts/mirrors/mirror_repos.js4
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.js4
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue11
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget_form.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/charts/anomaly.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/charts/column.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/charts/gauge.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/heatmap.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/options.js2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/single_stat.vue16
-rw-r--r--app/assets/javascripts/monitoring/components/charts/stacked_column.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue12
-rw-r--r--app/assets/javascripts/monitoring/components/create_dashboard_modal.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue31
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue15
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue28
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/embeds/embed_group.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/embeds/metric_embed.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/links_section.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/refresh_button.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue4
-rw-r--r--app/assets/javascripts/monitoring/constants.js2
-rw-r--r--app/assets/javascripts/monitoring/monitoring_app.js23
-rw-r--r--app/assets/javascripts/monitoring/pages/panel_new_page.vue4
-rw-r--r--app/assets/javascripts/monitoring/requests/index.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js24
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js8
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js4
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js8
-rw-r--r--app/assets/javascripts/monitoring/stores/variable_mapping.js2
-rw-r--r--app/assets/javascripts/monitoring/utils.js12
-rw-r--r--app/assets/javascripts/mr_notes/index.js13
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js4
-rw-r--r--app/assets/javascripts/mr_notes/stores/index.js2
-rw-r--r--app/assets/javascripts/mr_popover/components/mr_popover.vue2
-rw-r--r--app/assets/javascripts/mr_popover/index.js2
-rw-r--r--app/assets/javascripts/namespace_select.js4
-rw-r--r--app/assets/javascripts/namespaces/leave_by_url.js4
-rw-r--r--app/assets/javascripts/network/branch_graph.js2
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue2
-rw-r--r--app/assets/javascripts/notes.js33
-rw-r--r--app/assets/javascripts/notes/components/comment_field_layout.vue2
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue90
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue6
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_navigator.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue14
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue11
-rw-r--r--app/assets/javascripts/notes/components/email_participants_warning.vue2
-rw-r--r--app/assets/javascripts/notes/components/multiline_comment_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue117
-rw-r--r--app/assets/javascripts/notes/components/note_actions/reply_button.vue29
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue38
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue91
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue30
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue63
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue24
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue47
-rw-r--r--app/assets/javascripts/notes/components/sort_discussion.vue2
-rw-r--r--app/assets/javascripts/notes/components/timeline_toggle.vue2
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue25
-rw-r--r--app/assets/javascripts/notes/index.js2
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js2
-rw-r--r--app/assets/javascripts/notes/mixins/diff_line_note_form.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js50
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js3
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js38
-rw-r--r--app/assets/javascripts/notes/stores/utils.js4
-rw-r--r--app/assets/javascripts/notifications/components/custom_notifications_modal.vue128
-rw-r--r--app/assets/javascripts/notifications/components/notifications_dropdown.vue196
-rw-r--r--app/assets/javascripts/notifications/components/notifications_dropdown_item.vue42
-rw-r--r--app/assets/javascripts/notifications/constants.js58
-rw-r--r--app/assets/javascripts/notifications/index.js44
-rw-r--r--app/assets/javascripts/notifications_dropdown.js2
-rw-r--r--app/assets/javascripts/notifications_form.js4
-rw-r--r--app/assets/javascripts/onboarding_issues/index.js128
-rw-r--r--app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue2
-rw-r--r--app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue2
-rw-r--r--app/assets/javascripts/operation_settings/components/metrics_settings.vue8
-rw-r--r--app/assets/javascripts/operation_settings/index.js2
-rw-r--r--app/assets/javascripts/operation_settings/store/actions.js4
-rw-r--r--app/assets/javascripts/operation_settings/store/index.js2
-rw-r--r--app/assets/javascripts/packages/details/components/app.vue14
-rw-r--r--app/assets/javascripts/packages/details/components/installation_commands.vue4
-rw-r--r--app/assets/javascripts/packages/details/components/package_files.vue4
-rw-r--r--app/assets/javascripts/packages/details/components/package_history.vue6
-rw-r--r--app/assets/javascripts/packages/details/components/package_title.vue10
-rw-r--r--app/assets/javascripts/packages/details/index.js2
-rw-r--r--app/assets/javascripts/packages/details/store/actions.js2
-rw-r--r--app/assets/javascripts/packages/list/components/package_search.vue50
-rw-r--r--app/assets/javascripts/packages/list/components/package_title.vue2
-rw-r--r--app/assets/javascripts/packages/list/components/packages_filter.vue21
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list.vue8
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list_app.vue89
-rw-r--r--app/assets/javascripts/packages/list/components/packages_sort.vue60
-rw-r--r--app/assets/javascripts/packages/list/components/tokens/package_type_token.vue26
-rw-r--r--app/assets/javascripts/packages/list/constants.js9
-rw-r--r--app/assets/javascripts/packages/list/packages_list_app_bundle.js4
-rw-r--r--app/assets/javascripts/packages/list/stores/actions.js11
-rw-r--r--app/assets/javascripts/packages/list/stores/getters.js2
-rw-r--r--app/assets/javascripts/packages/list/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/packages/list/stores/mutations.js10
-rw-r--r--app/assets/javascripts/packages/list/stores/state.js5
-rw-r--r--app/assets/javascripts/packages/shared/components/package_list_row.vue10
-rw-r--r--app/assets/javascripts/packages/shared/components/package_tags.vue4
-rw-r--r--app/assets/javascripts/packages/shared/components/packages_list_loader.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/bundle.js8
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue131
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue113
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js31
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql9
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql8
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js22
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js11
-rw-r--r--app/assets/javascripts/pager.js2
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js2
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_previewer.js2
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js4
-rw-r--r--app/assets/javascripts/pages/admin/clusters/index/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/clusters/show/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/cohorts/index.js22
-rw-r--r--app/assets/javascripts/pages/admin/groups/new/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/integrations/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/index.js5
-rw-r--r--app/assets/javascripts/pages/admin/runners/index.js5
-rw-r--r--app/assets/javascripts/pages/admin/services/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue58
-rw-r--r--app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue30
-rw-r--r--app/assets/javascripts/pages/admin/users/index.js14
-rw-r--r--app/assets/javascripts/pages/dashboard/issues/index.js6
-rw-r--r--app/assets/javascripts/pages/dashboard/merge_requests/index.js4
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue2
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js8
-rw-r--r--app/assets/javascripts/pages/groups/boards/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js12
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js26
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js8
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/milestones/show/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js4
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/settings/badges/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js9
-rw-r--r--app/assets/javascripts/pages/groups/settings/integrations/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/settings/repository/show/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js19
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_tabs.js4
-rw-r--r--app/assets/javascripts/pages/help/index/index.js2
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/status/index.js (renamed from app/assets/javascripts/pages/import/bulk_imports/index.js)0
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue4
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue4
-rw-r--r--app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js3
-rw-r--r--app/assets/javascripts/pages/oauth/index.js3
-rw-r--r--app/assets/javascripts/pages/profiles/accounts/show/index.js2
-rw-r--r--app/assets/javascripts/pages/profiles/index.js3
-rw-r--r--app/assets/javascripts/pages/profiles/index/index.js2
-rw-r--r--app/assets/javascripts/pages/profiles/notifications/show/index.js4
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js6
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/activity/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/alert_management/details/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/browse/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/file/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/blame/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/boards/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/clusters/new/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/clusters/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/pipelines/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js23
-rw-r--r--app/assets/javascripts/pages/projects/commits/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/compare/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/compare/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js48
-rw-r--r--app/assets/javascripts/pages/projects/edit/mount_search_settings.js12
-rw-r--r--app/assets/javascripts/pages/projects/environments/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/environments/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/find_file/show/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue7
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue27
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/incidents/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js6
-rw-r--r--app/assets/javascripts/pages/projects/init_form.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/form.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js14
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js23
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/index.js19
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue4
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue27
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue27
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js12
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/index/index.js25
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js4
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js26
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/edit/check_form_state.js24
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js9
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js14
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/milestones/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue6
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js6
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/product_analytics/graphs/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/project.js15
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js92
-rw-r--r--app/assets/javascripts/pages/projects/releases/edit/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/releases/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/releases/new/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/releases/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/security/configuration/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/serverless/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/services/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/settings/operations/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js10
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue11
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue97
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js1
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js27
-rw-r--r--app/assets/javascripts/pages/projects/tags/index/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/tags/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/tags/releases/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/tags/remove_tag.js2
-rw-r--r--app/assets/javascripts/pages/projects/tags/show/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js2
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js2
-rw-r--r--app/assets/javascripts/pages/search/show/index.js4
-rw-r--r--app/assets/javascripts/pages/search/show/search.js54
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js6
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js4
-rw-r--r--app/assets/javascripts/pages/shared/mount_runner_instructions.js32
-rw-r--r--app/assets/javascripts/pages/shared/wikis/index.js8
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js8
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js16
-rw-r--r--app/assets/javascripts/performance_bar/components/add_request.vue2
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue21
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue17
-rw-r--r--app/assets/javascripts/performance_bar/index.js5
-rw-r--r--app/assets/javascripts/performance_bar/performance_bar_log.js2
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js2
-rw-r--r--app/assets/javascripts/persistent_user_callout.js4
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue114
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue100
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue (renamed from app/assets/javascripts/pipeline_editor/components/text_editor.vue)30
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue30
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue (renamed from app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue)2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue6
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue121
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue26
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js14
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql2
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql1
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js36
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue269
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue60
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue45
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag_graph.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue21
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue26
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js4
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue22
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/legacy_header_component.vue132
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue28
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue45
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue44
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue68
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue123
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/stage.vue139
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue40
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines_mixin.js (renamed from app/assets/javascripts/pipelines/mixins/pipelines.js)53
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js110
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_dag.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_graph.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js2
-rw-r--r--app/assets/javascripts/pipelines/pipelines_index.js6
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js12
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js12
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/index.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/state.js3
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/utils.js19
-rw-r--r--app/assets/javascripts/pipelines/utils.js6
-rw-r--r--app/assets/javascripts/popovers/index.js2
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue2
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue4
-rw-r--r--app/assets/javascripts/profile/account/index.js5
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue105
-rw-r--r--app/assets/javascripts/profile/preferences/constants.js22
-rw-r--r--app/assets/javascripts/profile/preferences/profile_preferences_bundle.js20
-rw-r--r--app/assets/javascripts/profile/profile.js5
-rw-r--r--app/assets/javascripts/project_find_file.js6
-rw-r--r--app/assets/javascripts/project_label_subscription.js6
-rw-r--r--app/assets/javascripts/project_select.js4
-rw-r--r--app/assets/javascripts/projects/commit/components/branches_dropdown.vue2
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue5
-rw-r--r--app/assets/javascripts/projects/commit/components/form_trigger.vue5
-rw-r--r--app/assets/javascripts/projects/commit/constants.js9
-rw-r--r--app/assets/javascripts/projects/commit/index.js11
-rw-r--r--app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js51
-rw-r--r--app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js20
-rw-r--r--app/assets/javascripts/projects/commit/init_revert_commit_modal.js4
-rw-r--r--app/assets/javascripts/projects/commit/init_revert_commit_trigger.js8
-rw-r--r--app/assets/javascripts/projects/commit/store/actions.js4
-rw-r--r--app/assets/javascripts/projects/commit_box/info/index.js4
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue6
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js8
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue89
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown.vue145
-rw-r--r--app/assets/javascripts/projects/compare/index.js33
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue4
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_button.vue4
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue6
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/index.js2
-rw-r--r--app/assets/javascripts/projects/members/constants.js1
-rw-r--r--app/assets/javascripts/projects/members/utils.js8
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue203
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue50
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue233
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/constants.js2
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js87
-rw-r--r--app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue2
-rw-r--r--app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js2
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue67
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue20
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/event_hub.js3
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js70
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js23
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue6
-rw-r--r--app/assets/javascripts/prometheus_alerts/components/reset_key.vue4
-rw-r--r--app/assets/javascripts/prometheus_metrics/custom_metrics.js6
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js2
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js10
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js6
-rw-r--r--app/assets/javascripts/protected_tags/constants.js3
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js2
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js4
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js9
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue4
-rw-r--r--app/assets/javascripts/ref/stores/mutations.js4
-rw-r--r--app/assets/javascripts/registry/explorer/components/delete_image.vue76
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue37
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/details_header.vue36
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue44
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue33
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue50
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue13
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue10
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue5
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue6
-rw-r--r--app/assets/javascripts/registry/explorer/constants/details.js64
-rw-r--r--app/assets/javascripts/registry/explorer/constants/list.js10
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql19
-rw-r--r--app/assets/javascripts/registry/explorer/index.js18
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue157
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue129
-rw-r--r--app/assets/javascripts/registry/explorer/router.js4
-rw-r--r--app/assets/javascripts/registry/settings/components/registry_settings_app.vue2
-rw-r--r--app/assets/javascripts/registry/settings/components/settings_form.vue6
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js4
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue4
-rw-r--r--app/assets/javascripts/related_issues/components/related_issuable_input.vue2
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue6
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_list.vue4
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_root.vue6
-rw-r--r--app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue2
-rw-r--r--app/assets/javascripts/related_merge_requests/store/actions.js4
-rw-r--r--app/assets/javascripts/related_merge_requests/store/index.js2
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue8
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue4
-rw-r--r--app/assets/javascripts/releases/components/asset_links_form.vue4
-rw-r--r--app/assets/javascripts/releases/components/evidence_block.vue6
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue4
-rw-r--r--app/assets/javascripts/releases/components/release_block_assets.vue2
-rw-r--r--app/assets/javascripts/releases/components/release_block_footer.vue2
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue19
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_graphql.vue2
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_rest.vue2
-rw-r--r--app/assets/javascripts/releases/components/tag_field_existing.vue4
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue2
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/actions.js6
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutations.js2
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/actions.js8
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/index.js2
-rw-r--r--app/assets/javascripts/releases/util.js2
-rw-r--r--app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue2
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/actions.js4
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/getters.js2
-rw-r--r--app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue2
-rw-r--r--app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue18
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/actions.js19
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/getters.js4
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/mutations.js5
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/state.js2
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js4
-rw-r--r--app/assets/javascripts/reports/components/grouped_issues_list.vue2
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue16
-rw-r--r--app/assets/javascripts/reports/components/issue_body.js2
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue2
-rw-r--r--app/assets/javascripts/reports/components/test_issue_body.vue2
-rw-r--r--app/assets/javascripts/reports/store/actions.js2
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue2
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue4
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue4
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue4
-rw-r--r--app/assets/javascripts/repository/graphql.js4
-rw-r--r--app/assets/javascripts/repository/index.js12
-rw-r--r--app/assets/javascripts/repository/mixins/preload.js2
-rw-r--r--app/assets/javascripts/repository/pages/index.vue2
-rw-r--r--app/assets/javascripts/repository/pages/tree.vue2
-rw-r--r--app/assets/javascripts/repository/router.js2
-rw-r--r--app/assets/javascripts/right_sidebar.js4
-rw-r--r--app/assets/javascripts/search/highlight_blob_search_result.js4
-rw-r--r--app/assets/javascripts/search/index.js15
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/components/radio_filter.vue2
-rw-r--r--app/assets/javascripts/search/sort/components/app.vue103
-rw-r--r--app/assets/javascripts/search/sort/constants.js19
-rw-r--r--app/assets/javascripts/search/sort/index.js27
-rw-r--r--app/assets/javascripts/search/store/actions.js2
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue73
-rw-r--r--app/assets/javascripts/search/topbar/components/group_filter.vue5
-rw-r--r--app/assets/javascripts/search/topbar/components/project_filter.vue5
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown.vue2
-rw-r--r--app/assets/javascripts/search/topbar/index.js31
-rw-r--r--app/assets/javascripts/search_autocomplete.js6
-rw-r--r--app/assets/javascripts/search_settings/components/search_settings.vue52
-rw-r--r--app/assets/javascripts/search_settings/index.js27
-rw-r--r--app/assets/javascripts/search_settings/mount.js23
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue23
-rw-r--r--app/assets/javascripts/security_configuration/components/configuration_table.vue97
-rw-r--r--app/assets/javascripts/security_configuration/components/features_constants.js112
-rw-r--r--app/assets/javascripts/security_configuration/components/manage_sast.vue57
-rw-r--r--app/assets/javascripts/security_configuration/components/upgrade.vue26
-rw-r--r--app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql6
-rw-r--r--app/assets/javascripts/security_configuration/index.js29
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue9
-rw-r--r--app/assets/javascripts/self_monitor/index.js2
-rw-r--r--app/assets/javascripts/self_monitor/store/actions.js4
-rw-r--r--app/assets/javascripts/self_monitor/store/index.js2
-rw-r--r--app/assets/javascripts/sentry/sentry_config.js2
-rw-r--r--app/assets/javascripts/sentry_error_stack_trace/index.js2
-rw-r--r--app/assets/javascripts/serverless/components/area.vue2
-rw-r--r--app/assets/javascripts/serverless/components/environment_row.vue2
-rw-r--r--app/assets/javascripts/serverless/components/function_details.vue4
-rw-r--r--app/assets/javascripts/serverless/components/function_row.vue2
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue28
-rw-r--r--app/assets/javascripts/serverless/serverless_bundle.js2
-rw-r--r--app/assets/javascripts/serverless/store/actions.js6
-rw-r--r--app/assets/javascripts/serverless/survey_banner.vue2
-rw-r--r--app/assets/javascripts/set_status_modal/components/user_availability_status.vue26
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue12
-rw-r--r--app/assets/javascripts/set_status_modal/utils.js6
-rw-r--r--app/assets/javascripts/settings_panels.js25
-rw-r--r--app/assets/javascripts/shared/milestones/form.js2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue54
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue16
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue37
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue403
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue40
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue128
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue95
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue15
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue4
-rw-r--r--app/assets/javascripts/sidebar/constants.js16
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js2
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js56
-rw-r--r--app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql5
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js15
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js2
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js16
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js8
-rw-r--r--app/assets/javascripts/single_file_diff.js8
-rw-r--r--app/assets/javascripts/snippet/snippet_edit.js4
-rw-r--r--app/assets/javascripts/snippet/snippet_show.js4
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue81
-rw-r--r--app/assets/javascripts/snippets/components/embed_dropdown.vue2
-rw-r--r--app/assets/javascripts/snippets/components/show.vue14
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue8
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_edit.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue6
-rw-r--r--app/assets/javascripts/snippets/components/snippet_visibility_edit.vue2
-rw-r--r--app/assets/javascripts/snippets/index.js2
-rw-r--r--app/assets/javascripts/snippets/mixins/snippets.js8
-rw-r--r--app/assets/javascripts/snippets/mutations/createSnippet.mutation.graphql2
-rw-r--r--app/assets/javascripts/snippets/mutations/updateSnippet.mutation.graphql3
-rw-r--r--app/assets/javascripts/snippets/utils/blob.js4
-rw-r--r--app/assets/javascripts/snippets/utils/error.js15
-rw-r--r--app/assets/javascripts/star.js4
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue12
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_drawer.vue7
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue5
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/index.js4
-rw-r--r--app/assets/javascripts/static_site_editor/image_repository.js2
-rw-r--r--app/assets/javascripts/static_site_editor/index.js2
-rw-r--r--app/assets/javascripts/static_site_editor/pages/home.vue8
-rw-r--r--app/assets/javascripts/static_site_editor/pages/success.vue2
-rw-r--r--app/assets/javascripts/static_site_editor/services/submit_content_changes.js2
-rw-r--r--app/assets/javascripts/subscription_select.js2
-rw-r--r--app/assets/javascripts/task_list.js2
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js27
-rw-r--r--app/assets/javascripts/terminal/terminal.js2
-rw-r--r--app/assets/javascripts/terraform/components/states_table.vue62
-rw-r--r--app/assets/javascripts/terraform/components/states_table_actions.vue80
-rw-r--r--app/assets/javascripts/terraform/components/terraform_list.vue2
-rw-r--r--app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql5
-rw-r--r--app/assets/javascripts/terraform/graphql/mutations/add_data_to_state.mutation.graphql3
-rw-r--r--app/assets/javascripts/terraform/graphql/resolvers.js45
-rw-r--r--app/assets/javascripts/terraform/index.js14
-rw-r--r--app/assets/javascripts/toggle_buttons.js2
-rw-r--r--app/assets/javascripts/tooltips/index.js94
-rw-r--r--app/assets/javascripts/ui_development_kit.js2
-rw-r--r--app/assets/javascripts/usage_ping_consent.js2
-rw-r--r--app/assets/javascripts/user_lists/components/edit_user_list.vue2
-rw-r--r--app/assets/javascripts/user_lists/components/new_user_list.vue2
-rw-r--r--app/assets/javascripts/user_lists/components/user_list.vue2
-rw-r--r--app/assets/javascripts/user_lists/store/edit/index.js2
-rw-r--r--app/assets/javascripts/user_lists/store/new/index.js2
-rw-r--r--app/assets/javascripts/user_lists/store/show/index.js2
-rw-r--r--app/assets/javascripts/user_lists/store/show/mutations.js2
-rw-r--r--app/assets/javascripts/users_select/index.js19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue37
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue26
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue45
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue38
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue63
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js24
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue (renamed from app/assets/javascripts/alert_management/components/alert_details.vue)48
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_metrics.vue (renamed from app/assets/javascripts/alert_management/components/alert_metrics.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue (renamed from app/assets/javascripts/alert_management/components/alert_sidebar.vue)12
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue (renamed from app/assets/javascripts/alert_management/components/alert_status.vue)14
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_summary_row.vue (renamed from app/assets/javascripts/alert_management/components/alert_summary_row.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue (renamed from app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue (renamed from app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue (renamed from app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue)2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue (renamed from app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue (renamed from app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue)6
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue (renamed from app/assets/javascripts/alert_management/components/system_notes/system_note.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/constants.js31
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/fragments/alert_detail_item.fragment.graphql (renamed from app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql (renamed from app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql (renamed from app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_sidebar_status.mutation.graphql (renamed from app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql (renamed from app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql)2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_details.query.graphql (renamed from app/assets/javascripts/alert_management/graphql/queries/details.query.graphql)2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_status.query.graphql (renamed from app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/index.js (renamed from app/assets/javascripts/alert_management/details.js)30
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/router.js (renamed from app/assets/javascripts/alert_management/router.js)0
-rw-r--r--app/assets/javascripts/vue_shared/components/actions_button.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/alert_details_table.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue61
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/editor_lite.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/item.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/code_instruction.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/registry_search.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/constants.js18
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql20
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql16
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue261
-rw-r--r--app/assets/javascripts/vue_shared/components/settings/settings_block.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql13
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql19
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql19
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql)18
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql21
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/todo_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue88
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue2
-rw-r--r--app/assets/javascripts/vue_shared/directives/validation.js2
-rw-r--r--app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js2
-rw-r--r--app/assets/javascripts/vue_shared/plugins/global_toast.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue108
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/getters.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/index.js4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/utils.js2
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/index.js4
-rw-r--r--app/assets/javascripts/webpack_non_compiled_placeholder.js24
-rw-r--r--app/assets/javascripts/whats_new/components/app.vue6
-rw-r--r--app/assets/javascripts/whats_new/store/actions.js2
-rw-r--r--app/assets/javascripts/zen_mode.js2
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/components/batch_comments/review_bar.scss2
-rw-r--r--app/assets/stylesheets/components/design_management/design_list_item.scss5
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/awards.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss23
-rw-r--r--app/assets/stylesheets/framework/carousel.scss202
-rw-r--r--app/assets/stylesheets/framework/ci_variable_list.scss10
-rw-r--r--app/assets/stylesheets/framework/common.scss2
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss2
-rw-r--r--app/assets/stylesheets/framework/diffs.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss24
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss51
-rw-r--r--app/assets/stylesheets/framework/files.scss38
-rw-r--r--app/assets/stylesheets/framework/filters.scss9
-rw-r--r--app/assets/stylesheets/framework/header.scss24
-rw-r--r--app/assets/stylesheets/framework/icons.scss48
-rw-r--r--app/assets/stylesheets/framework/mixins.scss7
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss2
-rw-r--r--app/assets/stylesheets/framework/tables.scss3
-rw-r--r--app/assets/stylesheets/framework/typography.scss7
-rw-r--r--app/assets/stylesheets/framework/variables.scss23
-rw-r--r--app/assets/stylesheets/mailer.scss23
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss22
-rw-r--r--app/assets/stylesheets/page_bundles/_pipeline_mixins.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/admin/application_settings_metrics_and_profiling.scss3
-rw-r--r--app/assets/stylesheets/page_bundles/admin/jobs_index.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss41
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss65
-rw-r--r--app/assets/stylesheets/page_bundles/import.scss29
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/oncall_schedules.scss12
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss11
-rw-r--r--app/assets/stylesheets/page_bundles/signup.scss4
-rw-r--r--app/assets/stylesheets/pages/admin.scss18
-rw-r--r--app/assets/stylesheets/pages/commits.scss5
-rw-r--r--app/assets/stylesheets/pages/editor.scss13
-rw-r--r--app/assets/stylesheets/pages/groups.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss13
-rw-r--r--app/assets/stylesheets/pages/issues.scss28
-rw-r--r--app/assets/stylesheets/pages/labels.scss2
-rw-r--r--app/assets/stylesheets/pages/login.scss3
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss20
-rw-r--r--app/assets/stylesheets/pages/note_form.scss6
-rw-r--r--app/assets/stylesheets/pages/notes.scss58
-rw-r--r--app/assets/stylesheets/pages/profile.scss20
-rw-r--r--app/assets/stylesheets/pages/projects.scss18
-rw-r--r--app/assets/stylesheets/pages/tree.scss4
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss2
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss3
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss5
-rw-r--r--app/assets/stylesheets/utilities.scss16
1521 files changed, 15363 insertions, 8854 deletions
diff --git a/app/assets/images/auth_buttons/openid_64.png b/app/assets/images/auth_buttons/openid_64.png
new file mode 100644
index 00000000000..7b53d129f95
--- /dev/null
+++ b/app/assets/images/auth_buttons/openid_64.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/create-0.png b/app/assets/images/mailers/in_product_marketing/create-0.png
new file mode 100644
index 00000000000..7fc992f14f2
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/create-0.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/create-1.png b/app/assets/images/mailers/in_product_marketing/create-1.png
new file mode 100644
index 00000000000..0315ffefb31
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/create-1.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/create-2.png b/app/assets/images/mailers/in_product_marketing/create-2.png
new file mode 100644
index 00000000000..619f9fcd659
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/create-2.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.png b/app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.png
new file mode 100644
index 00000000000..31083af512e
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/team-0.png b/app/assets/images/mailers/in_product_marketing/team-0.png
new file mode 100644
index 00000000000..f10ae998efa
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/team-0.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/team-1.png b/app/assets/images/mailers/in_product_marketing/team-1.png
new file mode 100644
index 00000000000..cd68464e6e8
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/team-1.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/team-2.png b/app/assets/images/mailers/in_product_marketing/team-2.png
new file mode 100644
index 00000000000..b199c659943
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/team-2.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/trial-0.png b/app/assets/images/mailers/in_product_marketing/trial-0.png
new file mode 100644
index 00000000000..3b0d7a8ecd8
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/trial-0.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/trial-1.png b/app/assets/images/mailers/in_product_marketing/trial-1.png
new file mode 100644
index 00000000000..3a30b2acaee
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/trial-1.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/trial-2.png b/app/assets/images/mailers/in_product_marketing/trial-2.png
new file mode 100644
index 00000000000..95bd965b49f
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/trial-2.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/verify-0.png b/app/assets/images/mailers/in_product_marketing/verify-0.png
new file mode 100644
index 00000000000..04b6f172b37
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/verify-0.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/verify-1.png b/app/assets/images/mailers/in_product_marketing/verify-1.png
new file mode 100644
index 00000000000..8997e8ba575
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/verify-1.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/verify-2.png b/app/assets/images/mailers/in_product_marketing/verify-2.png
new file mode 100644
index 00000000000..93c99dee246
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/verify-2.png
Binary files differ
diff --git a/app/assets/javascripts/actioncable_connection_monitor.js b/app/assets/javascripts/actioncable_connection_monitor.js
new file mode 100644
index 00000000000..fc4e436c7fb
--- /dev/null
+++ b/app/assets/javascripts/actioncable_connection_monitor.js
@@ -0,0 +1,142 @@
+/* eslint-disable no-restricted-globals */
+
+import { logger } from '@rails/actioncable';
+
+// This is based on https://github.com/rails/rails/blob/5a477890c809d4a17dc0dede43c6b8cef81d8175/actioncable/app/javascript/action_cable/connection_monitor.js
+// so that we can take advantage of the improved reconnection logic. We can remove this once we upgrade @rails/actioncable to a version that includes this.
+
+// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
+// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
+
+const now = () => new Date().getTime();
+
+const secondsSince = (time) => (now() - time) / 1000;
+class ConnectionMonitor {
+ constructor(connection) {
+ this.visibilityDidChange = this.visibilityDidChange.bind(this);
+ this.connection = connection;
+ this.reconnectAttempts = 0;
+ }
+
+ start() {
+ if (!this.isRunning()) {
+ this.startedAt = now();
+ delete this.stoppedAt;
+ this.startPolling();
+ addEventListener('visibilitychange', this.visibilityDidChange);
+ logger.log(
+ `ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`,
+ );
+ }
+ }
+
+ stop() {
+ if (this.isRunning()) {
+ this.stoppedAt = now();
+ this.stopPolling();
+ removeEventListener('visibilitychange', this.visibilityDidChange);
+ logger.log('ConnectionMonitor stopped');
+ }
+ }
+
+ isRunning() {
+ return this.startedAt && !this.stoppedAt;
+ }
+
+ recordPing() {
+ this.pingedAt = now();
+ }
+
+ recordConnect() {
+ this.reconnectAttempts = 0;
+ this.recordPing();
+ delete this.disconnectedAt;
+ logger.log('ConnectionMonitor recorded connect');
+ }
+
+ recordDisconnect() {
+ this.disconnectedAt = now();
+ logger.log('ConnectionMonitor recorded disconnect');
+ }
+
+ // Private
+
+ startPolling() {
+ this.stopPolling();
+ this.poll();
+ }
+
+ stopPolling() {
+ clearTimeout(this.pollTimeout);
+ }
+
+ poll() {
+ this.pollTimeout = setTimeout(() => {
+ this.reconnectIfStale();
+ this.poll();
+ }, this.getPollInterval());
+ }
+
+ getPollInterval() {
+ const { staleThreshold, reconnectionBackoffRate } = this.constructor;
+ const backoff = (1 + reconnectionBackoffRate) ** Math.min(this.reconnectAttempts, 10);
+ const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate;
+ const jitter = jitterMax * Math.random();
+ return staleThreshold * 1000 * backoff * (1 + jitter);
+ }
+
+ reconnectIfStale() {
+ if (this.connectionIsStale()) {
+ logger.log(
+ `ConnectionMonitor detected stale connection. reconnectAttempts = ${
+ this.reconnectAttempts
+ }, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${
+ this.constructor.staleThreshold
+ } s`,
+ );
+ this.reconnectAttempts += 1;
+ if (this.disconnectedRecently()) {
+ logger.log(
+ `ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(
+ this.disconnectedAt,
+ )} s`,
+ );
+ } else {
+ logger.log('ConnectionMonitor reopening');
+ this.connection.reopen();
+ }
+ }
+ }
+
+ get refreshedAt() {
+ return this.pingedAt ? this.pingedAt : this.startedAt;
+ }
+
+ connectionIsStale() {
+ return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
+ }
+
+ disconnectedRecently() {
+ return (
+ this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold
+ );
+ }
+
+ visibilityDidChange() {
+ if (document.visibilityState === 'visible') {
+ setTimeout(() => {
+ if (this.connectionIsStale() || !this.connection.isOpen()) {
+ logger.log(
+ `ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`,
+ );
+ this.connection.reopen();
+ }
+ }, 200);
+ }
+ }
+}
+
+ConnectionMonitor.staleThreshold = 6; // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
+ConnectionMonitor.reconnectionBackoffRate = 0.15;
+
+export default ConnectionMonitor;
diff --git a/app/assets/javascripts/actioncable_consumer.js b/app/assets/javascripts/actioncable_consumer.js
index 5658ffc1a38..aeb61e61a3d 100644
--- a/app/assets/javascripts/actioncable_consumer.js
+++ b/app/assets/javascripts/actioncable_consumer.js
@@ -1,3 +1,10 @@
import { createConsumer } from '@rails/actioncable';
+import ConnectionMonitor from './actioncable_connection_monitor';
-export default createConsumer();
+const consumer = createConsumer();
+
+if (consumer.connection) {
+ consumer.connection.monitor = new ConnectionMonitor(consumer.connection);
+}
+
+export default consumer;
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index 6b9f46dcfb6..5064d9ee2d2 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -2,8 +2,8 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
-import Pager from './pager';
import { localTimeAgo } from './lib/utils/datetime_utility';
+import Pager from './pager';
export default class Activities {
constructor(container = '') {
diff --git a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
index c58ded3f1f5..5d074698ea4 100644
--- a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
+++ b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
@@ -1,10 +1,11 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlModal, GlTabs, GlTab, GlSearchBoxByType, GlSprintf } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
findCommitIndex,
setCommitStatus,
@@ -119,7 +120,7 @@ export default {
openModal() {
this.searchCommits();
this.fetchContextCommits();
- this.$root.$emit('bv::show::modal', 'add-review-item');
+ this.$root.$emit(BV_SHOW_MODAL, 'add-review-item');
},
handleTabChange(tabIndex) {
if (tabIndex === 0) {
diff --git a/app/assets/javascripts/add_context_commits_modal/index.js b/app/assets/javascripts/add_context_commits_modal/index.js
index b5cd111fabc..697d32664e8 100644
--- a/app/assets/javascripts/add_context_commits_modal/index.js
+++ b/app/assets/javascripts/add_context_commits_modal/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
-import createStore from './store';
import AddContextCommitsModalTrigger from './components/add_context_commits_modal_trigger.vue';
import AddContextCommitsModalWrapper from './components/add_context_commits_modal_wrapper.vue';
+import createStore from './store';
export default function initAddContextCommitsTriggers() {
const addContextCommitsModalTriggerEl = document.querySelector('.add-review-item-modal-trigger');
diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js
index 1bf54b159ee..7b6f4c81bd2 100644
--- a/app/assets/javascripts/add_context_commits_modal/store/actions.js
+++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js
@@ -1,8 +1,8 @@
import _ from 'lodash';
-import axios from '~/lib/utils/axios_utils';
+import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
-import Api from '~/api';
import * as types from './mutation_types';
export const setBaseConfig = ({ commit }, options) => {
diff --git a/app/assets/javascripts/admin/statistics_panel/components/app.vue b/app/assets/javascripts/admin/statistics_panel/components/app.vue
index 29077d926cf..1f0db422807 100644
--- a/app/assets/javascripts/admin/statistics_panel/components/app.vue
+++ b/app/assets/javascripts/admin/statistics_panel/components/app.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import { mapState, mapGetters, mapActions } from 'vuex';
import statisticsLabels from '../constants';
export default {
@@ -26,8 +26,8 @@ export default {
</script>
<template>
- <div class="info-well">
- <div class="well-segment admin-well admin-well-statistics">
+ <div class="gl-card">
+ <div class="gl-card-body">
<h4>{{ __('Statistics') }}</h4>
<gl-loading-icon v-if="isLoading" size="md" class="my-3" />
<template v-else>
diff --git a/app/assets/javascripts/admin/statistics_panel/index.js b/app/assets/javascripts/admin/statistics_panel/index.js
index 8c49fffe630..2f8c3d2e167 100644
--- a/app/assets/javascripts/admin/statistics_panel/index.js
+++ b/app/assets/javascripts/admin/statistics_panel/index.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import StatisticsPanelApp from './components/app.vue';
import createStore from './store';
-export default function (el) {
+export default function initStatisticsPanel(el) {
if (!el) {
return false;
}
diff --git a/app/assets/javascripts/admin/statistics_panel/store/actions.js b/app/assets/javascripts/admin/statistics_panel/store/actions.js
index 149540c4222..459f11c02f1 100644
--- a/app/assets/javascripts/admin/statistics_panel/store/actions.js
+++ b/app/assets/javascripts/admin/statistics_panel/store/actions.js
@@ -1,7 +1,7 @@
import Api from '~/api';
-import { s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
import * as types from './mutation_types';
export const requestStatistics = ({ commit }) => commit(types.REQUEST_STATISTICS);
diff --git a/app/assets/javascripts/admin/users/components/actions/activate.vue b/app/assets/javascripts/admin/users/components/actions/activate.vue
new file mode 100644
index 00000000000..99c260bf11e
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/activate.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ modalAttributes() {
+ return {
+ 'data-path': this.path,
+ 'data-method': 'put',
+ 'data-modal-attributes': JSON.stringify({
+ title: sprintf(s__('AdminUsers|Activate user %{username}?'), {
+ username: this.username,
+ }),
+ message: s__('AdminUsers|You can always deactivate their account again if needed.'),
+ okVariant: 'confirm',
+ okTitle: s__('AdminUsers|Activate'),
+ }),
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item>
+ <slot></slot>
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/approve.vue b/app/assets/javascripts/admin/users/components/actions/approve.vue
new file mode 100644
index 00000000000..6fc43c246ea
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/approve.vue
@@ -0,0 +1,21 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-item :href="path" data-method="put">
+ <slot></slot>
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/block.vue b/app/assets/javascripts/admin/users/components/actions/block.vue
new file mode 100644
index 00000000000..68dfefe14c2
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/block.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `
+ <p>${s__('AdminUsers|Blocking user has the following effects:')}</p>
+ <ul>
+ <li>${s__('AdminUsers|User will not be able to login')}</li>
+ <li>${s__('AdminUsers|User will not be able to access git repositories')}</li>
+ <li>${s__('AdminUsers|Personal projects will be left')}</li>
+ <li>${s__('AdminUsers|Owned groups will be left')}</li>
+ </ul>
+`;
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ modalAttributes() {
+ return {
+ 'data-path': this.path,
+ 'data-method': 'put',
+ 'data-modal-attributes': JSON.stringify({
+ title: sprintf(s__('AdminUsers|Block user %{username}?'), { username: this.username }),
+ okVariant: 'confirm',
+ okTitle: s__('AdminUsers|Block'),
+ messageHtml,
+ }),
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item>
+ <slot></slot>
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/deactivate.vue b/app/assets/javascripts/admin/users/components/actions/deactivate.vue
new file mode 100644
index 00000000000..7e0c17ba296
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/deactivate.vue
@@ -0,0 +1,60 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `
+ <p>${s__('AdminUsers|Deactivating a user has the following effects:')}</p>
+ <ul>
+ <li>${s__('AdminUsers|The user will be logged out')}</li>
+ <li>${s__('AdminUsers|The user will not be able to access git repositories')}</li>
+ <li>${s__('AdminUsers|The user will not be able to access the API')}</li>
+ <li>${s__('AdminUsers|The user will not receive any notifications')}</li>
+ <li>${s__('AdminUsers|The user will not be able to use slash commands')}</li>
+ <li>${s__(
+ 'AdminUsers|When the user logs back in, their account will reactivate as a fully active account',
+ )}</li>
+ <li>${s__('AdminUsers|Personal projects, group and user history will be left intact')}</li>
+ </ul>
+`;
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ modalAttributes() {
+ return {
+ 'data-path': this.path,
+ 'data-method': 'put',
+ 'data-modal-attributes': JSON.stringify({
+ title: sprintf(s__('AdminUsers|Deactivate user %{username}?'), {
+ username: this.username,
+ }),
+ okVariant: 'confirm',
+ okTitle: s__('AdminUsers|Deactivate'),
+ messageHtml,
+ }),
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item>
+ <slot></slot>
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/delete.vue b/app/assets/javascripts/admin/users/components/actions/delete.vue
new file mode 100644
index 00000000000..725d3dbf388
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/delete.vue
@@ -0,0 +1,25 @@
+<script>
+import SharedDeleteAction from './shared/shared_delete_action.vue';
+
+export default {
+ components: {
+ SharedDeleteAction,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ paths: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <shared-delete-action modal-type="delete" :username="username" :paths="paths">
+ <slot></slot>
+ </shared-delete-action>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
new file mode 100644
index 00000000000..0ae15bfbebb
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
@@ -0,0 +1,25 @@
+<script>
+import SharedDeleteAction from './shared/shared_delete_action.vue';
+
+export default {
+ components: {
+ SharedDeleteAction,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ paths: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <shared-delete-action modal-type="delete-with-contributions" :username="username" :paths="paths">
+ <slot></slot>
+ </shared-delete-action>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/index.js b/app/assets/javascripts/admin/users/components/actions/index.js
new file mode 100644
index 00000000000..e34b01346b9
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/index.js
@@ -0,0 +1,21 @@
+import Activate from './activate.vue';
+import Approve from './approve.vue';
+import Block from './block.vue';
+import Deactivate from './deactivate.vue';
+import Delete from './delete.vue';
+import DeleteWithContributions from './delete_with_contributions.vue';
+import Reject from './reject.vue';
+import Unblock from './unblock.vue';
+import Unlock from './unlock.vue';
+
+export default {
+ Activate,
+ Approve,
+ Block,
+ Deactivate,
+ Delete,
+ DeleteWithContributions,
+ Unblock,
+ Unlock,
+ Reject,
+};
diff --git a/app/assets/javascripts/admin/users/components/actions/reject.vue b/app/assets/javascripts/admin/users/components/actions/reject.vue
new file mode 100644
index 00000000000..a80c1ff5458
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/reject.vue
@@ -0,0 +1,21 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-item :href="path" data-method="delete">
+ <slot></slot>
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue b/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
new file mode 100644
index 00000000000..9107d9ccdd9
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
@@ -0,0 +1,43 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ paths: {
+ type: Object,
+ required: true,
+ },
+ modalType: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ modalAttributes() {
+ return {
+ 'data-block-user-url': this.paths.block,
+ 'data-delete-user-url': this.paths.delete,
+ 'data-gl-modal-action': this.modalType,
+ 'data-username': this.username,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="js-delete-user-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item>
+ <span class="gl-text-red-500">
+ <slot></slot>
+ </span>
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/unblock.vue b/app/assets/javascripts/admin/users/components/actions/unblock.vue
new file mode 100644
index 00000000000..f2b501caf09
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/unblock.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ modalAttributes() {
+ return {
+ 'data-path': this.path,
+ 'data-method': 'put',
+ 'data-modal-attributes': JSON.stringify({
+ title: sprintf(s__('AdminUsers|Unblock user %{username}?'), { username: this.username }),
+ message: s__(
+ 'AdminUsers|You can always unblock their account, their data will remain intact.',
+ ),
+ okVariant: 'confirm',
+ okTitle: s__('AdminUsers|Unblock'),
+ }),
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item>
+ <slot></slot>
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/unlock.vue b/app/assets/javascripts/admin/users/components/actions/unlock.vue
new file mode 100644
index 00000000000..294aaade7c1
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/unlock.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { sprintf, s__, __ } from '~/locale';
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ modalAttributes() {
+ return {
+ 'data-path': this.path,
+ 'data-method': 'put',
+ 'data-modal-attributes': JSON.stringify({
+ title: sprintf(s__('AdminUsers|Unlock user %{username}?'), { username: this.username }),
+ message: __('Are you sure?'),
+ okVariant: 'confirm',
+ okTitle: s__('AdminUsers|Unlock'),
+ }),
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <gl-dropdown-item>
+ <slot></slot>
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue b/app/assets/javascripts/admin/users/components/usage_ping_disabled.vue
index 5da38495010..5da38495010 100644
--- a/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue
+++ b/app/assets/javascripts/admin/users/components/usage_ping_disabled.vue
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
new file mode 100644
index 00000000000..e92c97b54a3
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -0,0 +1,119 @@
+<script>
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
+} from '@gitlab/ui';
+import { convertArrayToCamelCase } from '~/lib/utils/common_utils';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { I18N_USER_ACTIONS } from '../constants';
+import { generateUserPaths } from '../utils';
+import Actions from './actions';
+
+export default {
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
+ ...Actions,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ paths: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ userActions() {
+ return convertArrayToCamelCase(this.user.actions);
+ },
+ dropdownActions() {
+ return this.userActions.filter((a) => a !== 'edit');
+ },
+ dropdownDeleteActions() {
+ return this.dropdownActions.filter((a) => a.includes('delete'));
+ },
+ dropdownSafeActions() {
+ return this.dropdownActions.filter((a) => !this.dropdownDeleteActions.includes(a));
+ },
+ hasDropdownActions() {
+ return this.dropdownActions.length > 0;
+ },
+ hasDeleteActions() {
+ return this.dropdownDeleteActions.length > 0;
+ },
+ hasEditAction() {
+ return this.userActions.includes('edit');
+ },
+ userPaths() {
+ return generateUserPaths(this.paths, this.user.username);
+ },
+ },
+ methods: {
+ isLdapAction(action) {
+ return action === 'ldapBlocked';
+ },
+ getActionComponent(action) {
+ return Actions[capitalizeFirstCharacter(action)];
+ },
+ },
+ i18n: I18N_USER_ACTIONS,
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button v-if="hasEditAction" data-testid="edit" :href="userPaths.edit">{{
+ $options.i18n.edit
+ }}</gl-button>
+
+ <gl-dropdown
+ v-if="hasDropdownActions"
+ data-testid="actions"
+ right
+ class="gl-ml-2"
+ icon="settings"
+ >
+ <gl-dropdown-section-header>{{ $options.i18n.settings }}</gl-dropdown-section-header>
+
+ <template v-for="action in dropdownSafeActions">
+ <component
+ :is="getActionComponent(action)"
+ v-if="getActionComponent(action)"
+ :key="action"
+ :path="userPaths[action]"
+ :username="user.name"
+ :data-testid="action"
+ >
+ {{ $options.i18n[action] }}
+ </component>
+ <gl-dropdown-item v-else-if="isLdapAction(action)" :key="action" :data-testid="action">
+ {{ $options.i18n[action] }}
+ </gl-dropdown-item>
+ </template>
+
+ <gl-dropdown-divider v-if="hasDeleteActions" />
+
+ <template v-for="action in dropdownDeleteActions">
+ <component
+ :is="getActionComponent(action)"
+ v-if="getActionComponent(action)"
+ :key="action"
+ :paths="userPaths"
+ :username="user.name"
+ :data-testid="`delete-${action}`"
+ >
+ {{ $options.i18n[action] }}
+ </component>
+ </template>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/user_avatar.vue b/app/assets/javascripts/admin/users/components/user_avatar.vue
index 4f79c4fd451..ce22595609d 100644
--- a/app/assets/javascripts/admin/users/components/user_avatar.vue
+++ b/app/assets/javascripts/admin/users/components/user_avatar.vue
@@ -1,12 +1,16 @@
<script>
-import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui';
-import { USER_AVATAR_SIZE } from '../constants';
+import { GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { truncate } from '~/lib/utils/text_utility';
+import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from '../constants';
export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
components: {
- GlAvatarLink,
GlAvatarLabeled,
GlBadge,
+ GlIcon,
},
props: {
user: {
@@ -22,16 +26,23 @@ export default {
adminUserHref() {
return this.adminUserPath.replace('id', this.user.username);
},
+ adminUserMailto() {
+ // NOTE: 'mailto:' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `mailto:${this.user.email}`;
+ },
+ userNoteShort() {
+ return truncate(this.user.note, LENGTH_OF_USER_NOTE_TOOLTIP);
+ },
},
USER_AVATAR_SIZE,
};
</script>
<template>
- <gl-avatar-link
+ <div
v-if="user"
- class="js-user-link"
- :href="adminUserHref"
+ class="js-user-link gl-display-inline-block"
:data-user-id="user.id"
:data-username="user.username"
>
@@ -40,8 +51,13 @@ export default {
:src="user.avatarUrl"
:label="user.name"
:sub-label="user.email"
+ :label-link="adminUserHref"
+ :sub-label-link="adminUserMailto"
>
<template #meta>
+ <div v-if="user.note" class="gl-text-gray-500 gl-p-1">
+ <gl-icon v-gl-tooltip="userNoteShort" name="document" />
+ </div>
<div v-for="(badge, idx) in user.badges" :key="idx" class="gl-p-1">
<gl-badge class="gl-display-flex!" size="sm" :variant="badge.variant">{{
badge.text
@@ -49,5 +65,5 @@ export default {
</div>
</template>
</gl-avatar-labeled>
- </gl-avatar-link>
+ </div>
</template>
diff --git a/app/assets/javascripts/admin/users/components/user_date.vue b/app/assets/javascripts/admin/users/components/user_date.vue
new file mode 100644
index 00000000000..38dddbf72c2
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/user_date.vue
@@ -0,0 +1,29 @@
+<script>
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
+import { SHORT_DATE_FORMAT } from '../constants';
+
+export default {
+ props: {
+ date: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ formattedDate() {
+ const { date } = this;
+ if (date === null) {
+ return __('Never');
+ }
+ return formatDate(new Date(date), SHORT_DATE_FORMAT);
+ },
+ },
+};
+</script>
+<template>
+ <span>
+ {{ formattedDate }}
+ </span>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue
index 15e31935a4c..8962068601c 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/admin/users/components/users_table.vue
@@ -1,7 +1,9 @@
<script>
import { GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
+import UserActions from './user_actions.vue';
import UserAvatar from './user_avatar.vue';
+import UserDate from './user_date.vue';
const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
@@ -11,6 +13,8 @@ export default {
components: {
GlTable,
UserAvatar,
+ UserActions,
+ UserDate,
},
props: {
users: {
@@ -62,7 +66,19 @@ export default {
stacked="md"
>
<template #cell(name)="{ item: user }">
- <UserAvatar :user="user" :admin-user-path="paths.adminUser" />
+ <user-avatar :user="user" :admin-user-path="paths.adminUser" />
+ </template>
+
+ <template #cell(createdAt)="{ item: { createdAt } }">
+ <user-date :date="createdAt" />
+ </template>
+
+ <template #cell(lastActivityOn)="{ item: { lastActivityOn } }">
+ <user-date :date="lastActivityOn" show-never />
+ </template>
+
+ <template #cell(settings)="{ item: user }">
+ <user-actions :user="user" :paths="paths" />
</template>
</gl-table>
</div>
diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js
index 675fcf00c39..8ea1bd3ca7a 100644
--- a/app/assets/javascripts/admin/users/constants.js
+++ b/app/assets/javascripts/admin/users/constants.js
@@ -1 +1,22 @@
+import { s__, __ } from '~/locale';
+
export const USER_AVATAR_SIZE = 32;
+
+export const SHORT_DATE_FORMAT = 'd mmm, yyyy';
+
+export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
+
+export const I18N_USER_ACTIONS = {
+ edit: __('Edit'),
+ settings: __('Settings'),
+ unlock: __('Unlock'),
+ block: s__('AdminUsers|Block'),
+ unblock: s__('AdminUsers|Unblock'),
+ approve: s__('AdminUsers|Approve'),
+ reject: s__('AdminUsers|Reject'),
+ deactivate: s__('AdminUsers|Deactivate'),
+ activate: s__('AdminUsers|Activate'),
+ ldapBlocked: s__('AdminUsers|Cannot unblock LDAP blocked users'),
+ delete: s__('AdminUsers|Delete user'),
+ deleteWithContributions: s__('AdminUsers|Delete user and contributions'),
+};
diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js
index f35b57c4e1a..0365d054fc9 100644
--- a/app/assets/javascripts/admin/users/index.js
+++ b/app/assets/javascripts/admin/users/index.js
@@ -1,8 +1,9 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import AdminUsersApp from './components/app.vue';
+import UsagePingDisabled from './components/usage_ping_disabled.vue';
-export default function (el = document.querySelector('#js-admin-users-app')) {
+export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => {
if (!el) {
return false;
}
@@ -19,4 +20,24 @@ export default function (el = document.querySelector('#js-admin-users-app')) {
},
}),
});
-}
+};
+
+export const initCohortsEmptyState = (el = document.querySelector('#js-cohorts-empty-state')) => {
+ if (!el) {
+ return false;
+ }
+
+ const { emptyStateSvgPath, enableUsagePingLink, docsLink } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ svgPath: emptyStateSvgPath,
+ primaryButtonPath: enableUsagePingLink,
+ docsLink,
+ },
+ render(h) {
+ return h(UsagePingDisabled);
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/users/tabs.js b/app/assets/javascripts/admin/users/tabs.js
new file mode 100644
index 00000000000..9ada77396c7
--- /dev/null
+++ b/app/assets/javascripts/admin/users/tabs.js
@@ -0,0 +1,23 @@
+import { historyPushState } from '~/lib/utils/common_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+
+const COHORTS_PANE = 'cohorts';
+
+const tabClickHandler = (e) => {
+ const { hash } = e.currentTarget;
+ const tab = hash === `#${COHORTS_PANE}` ? COHORTS_PANE : null;
+ const newUrl = mergeUrlParams({ tab }, window.location.href);
+ historyPushState(newUrl);
+};
+
+const initTabs = () => {
+ const tabLinks = document.querySelectorAll('.js-users-tab-item a');
+
+ if (tabLinks.length) {
+ tabLinks.forEach((tabLink) => {
+ tabLink.addEventListener('click', (e) => tabClickHandler(e));
+ });
+ }
+};
+
+export default initTabs;
diff --git a/app/assets/javascripts/admin/users/utils.js b/app/assets/javascripts/admin/users/utils.js
new file mode 100644
index 00000000000..f6c1091ba27
--- /dev/null
+++ b/app/assets/javascripts/admin/users/utils.js
@@ -0,0 +1,7 @@
+export const generateUserPaths = (paths, id) => {
+ return Object.fromEntries(
+ Object.entries(paths).map(([action, genericPath]) => {
+ return [action, genericPath.replace('id', id)];
+ }),
+ );
+};
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index 2bad15faa85..dd702c4a5d3 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -11,26 +11,22 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
-import { s__, __ } from '~/locale';
+import getAlertsQuery from '~/graphql_shared/queries/get_alerts.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
+import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
-import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
+import { s__, __ } from '~/locale';
+import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
import {
tdClass,
thClass,
bodyTrClass,
initialPaginationState,
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
+import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import { convertToSnakeCase } from '~/lib/utils/text_utility';
-import getAlertsQuery from '~/graphql_shared/queries/get_alerts.query.graphql';
+import { ALERTS_STATUS_TABS, SEVERITY_LEVELS, trackAlertListViewsOptions } from '../constants';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
-import {
- ALERTS_STATUS_TABS,
- ALERTS_SEVERITY_LABELS,
- trackAlertListViewsOptions,
-} from '../constants';
-import AlertStatus from './alert_status.vue';
const TH_TEST_ID = { 'data-testid': 'alert-management-severity-sort' };
@@ -96,7 +92,7 @@ export default {
sortable: true,
},
],
- severityLabels: ALERTS_SEVERITY_LABELS,
+ severityLabels: SEVERITY_LEVELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
GlAlert,
diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js
index b79a64646eb..c98d3865621 100644
--- a/app/assets/javascripts/alert_management/constants.js
+++ b/app/assets/javascripts/alert_management/constants.js
@@ -1,12 +1,12 @@
import { s__ } from '~/locale';
-export const ALERTS_SEVERITY_LABELS = {
- CRITICAL: s__('AlertManagement|Critical'),
- HIGH: s__('AlertManagement|High'),
- MEDIUM: s__('AlertManagement|Medium'),
- LOW: s__('AlertManagement|Low'),
- INFO: s__('AlertManagement|Info'),
- UNKNOWN: s__('AlertManagement|Unknown'),
+export const SEVERITY_LEVELS = {
+ CRITICAL: s__('severity|Critical'),
+ HIGH: s__('severity|High'),
+ MEDIUM: s__('severity|Medium'),
+ LOW: s__('severity|Low'),
+ INFO: s__('severity|Info'),
+ UNKNOWN: s__('severity|Unknown'),
};
export const ALERTS_STATUS_TABS = [
@@ -46,20 +46,3 @@ export const trackAlertListViewsOptions = {
category: 'Alert Management',
action: 'view_alerts_list',
};
-
-/**
- * Tracks snowplow event when user views alert details
- */
-export const trackAlertsDetailsViewsOptions = {
- category: 'Alert Management',
- action: 'view_alert_details',
-};
-
-/**
- * Tracks snowplow event when alert status is updated
- */
-export const trackAlertStatusUpdateOptions = {
- category: 'Alert Management',
- action: 'update_alert_status',
- label: 'Status',
-};
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
index b484841ed2c..f5eac26431f 100644
--- a/app/assets/javascripts/alert_management/list.js
+++ b/app/assets/javascripts/alert_management/list.js
@@ -1,8 +1,9 @@
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { PAGE_CONFIG } from '~/vue_shared/alert_details/constants';
import AlertManagementList from './components/alert_management_list_wrapper.vue';
Vue.use(VueApollo);
@@ -59,6 +60,7 @@ export default () => {
populatingAlertsHelpUrl,
emptyAlertSvgPath,
alertManagementEnabled: parseBoolean(alertManagementEnabled),
+ trackAlertStatusUpdateOptions: PAGE_CONFIG.OPERATIONS.TRACK_ALERT_STATUS_UPDATE_OPTIONS,
userCanEnableAlertManagement: parseBoolean(userCanEnableAlertManagement),
},
apolloProvider,
diff --git a/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue b/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue
deleted file mode 100644
index c0e93d315a4..00000000000
--- a/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue
+++ /dev/null
@@ -1,187 +0,0 @@
-<script>
-import {
- GlButton,
- GlFormGroup,
- GlFormInput,
- GlLink,
- GlModal,
- GlModalDirective,
- GlSprintf,
-} from '@gitlab/ui';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
-import axios from '~/lib/utils/axios_utils';
-import { s__, __ } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-
-export default {
- i18n: {
- usageSection: s__(
- 'AlertService|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page.',
- ),
- setupSection: s__(
- "AlertService|Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint.",
- ),
- },
- COPY_TO_CLIPBOARD: __('Copy'),
- RESET_KEY: __('Reset key'),
- components: {
- GlButton,
- GlFormGroup,
- GlFormInput,
- GlLink,
- GlModal,
- GlSprintf,
- ClipboardButton,
- ToggleButton,
- },
- directives: {
- 'gl-modal': GlModalDirective,
- },
- props: {
- alertsSetupUrl: {
- type: String,
- required: true,
- },
- alertsUsageUrl: {
- type: String,
- required: true,
- },
- initialAuthorizationKey: {
- type: String,
- required: false,
- default: '',
- },
- formPath: {
- type: String,
- required: true,
- },
- url: {
- type: String,
- required: true,
- },
- initialActivated: {
- type: Boolean,
- required: true,
- },
- isDisabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- activated: this.initialActivated,
- loadingActivated: false,
- authorizationKey: this.initialAuthorizationKey,
- };
- },
- computed: {
- sections() {
- return [
- {
- text: this.$options.i18n.usageSection,
- url: this.alertsUsageUrl,
- },
- {
- text: this.$options.i18n.setupSection,
- url: this.alertsSetupUrl,
- },
- ];
- },
- },
- methods: {
- resetKey() {
- return axios
- .put(this.formPath, { service: { token: '' } })
- .then((res) => {
- this.authorizationKey = res.data.token;
- })
- .catch(() => {
- createFlash(__('Failed to reset key. Please try again.'));
- });
- },
- toggleActivated(value) {
- this.loadingActivated = true;
- return axios
- .put(this.formPath, { service: { active: value } })
- .then(() => {
- this.activated = value;
- this.loadingActivated = false;
- })
- .catch(() => {
- createFlash(__('Update failed. Please try again.'));
- this.loadingActivated = false;
- });
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div data-testid="description">
- <p v-for="section in sections" :key="section.text">
- <gl-sprintf :message="section.text">
- <template #link="{ content }">
- <gl-link :href="section.url" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- <gl-form-group :label="__('Active')" label-for="activated" label-class="label-bold">
- <toggle-button
- id="activated"
- :disabled-input="loadingActivated || isDisabled"
- :is-loading="loadingActivated"
- :value="activated"
- @change="toggleActivated"
- />
- </gl-form-group>
- <gl-form-group :label="__('URL')" label-for="url" label-class="label-bold">
- <div class="input-group">
- <gl-form-input id="url" :readonly="true" :value="url" />
- <span class="input-group-append">
- <clipboard-button
- :text="url"
- :title="$options.COPY_TO_CLIPBOARD"
- :disabled="isDisabled"
- />
- </span>
- </div>
- </gl-form-group>
- <gl-form-group
- :label="__('Authorization key')"
- label-for="authorization-key"
- label-class="label-bold"
- >
- <div class="input-group">
- <gl-form-input id="authorization-key" :readonly="true" :value="authorizationKey" />
- <span class="input-group-append">
- <clipboard-button
- :text="authorizationKey"
- :title="$options.COPY_TO_CLIPBOARD"
- :disabled="isDisabled"
- />
- </span>
- </div>
- <gl-button v-gl-modal.authKeyModal class="gl-mt-2" :disabled="isDisabled">{{
- $options.RESET_KEY
- }}</gl-button>
- <gl-modal
- modal-id="authKeyModal"
- :title="$options.RESET_KEY"
- :ok-title="$options.RESET_KEY"
- ok-variant="danger"
- @ok="resetKey"
- >
- {{
- __(
- 'Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
- )
- }}
- </gl-modal>
- </gl-form-group>
- </div>
-</template>
diff --git a/app/assets/javascripts/alerts_service_settings/index.js b/app/assets/javascripts/alerts_service_settings/index.js
deleted file mode 100644
index ddba966ffb3..00000000000
--- a/app/assets/javascripts/alerts_service_settings/index.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import AlertsServiceForm from './components/alerts_service_form.vue';
-
-export default (el) => {
- if (!el) {
- return null;
- }
-
- const {
- activated: activatedStr,
- alertsSetupUrl,
- alertsUsageUrl,
- formPath,
- authorizationKey,
- url,
- disabled,
- } = el.dataset;
-
- const activated = parseBoolean(activatedStr);
- const isDisabled = parseBoolean(disabled);
-
- return new Vue({
- el,
- render(createElement) {
- return createElement(AlertsServiceForm, {
- props: {
- alertsSetupUrl,
- alertsUsageUrl,
- initialActivated: activated,
- formPath,
- initialAuthorizationKey: authorizationKey,
- url,
- isDisabled,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
index c52e9f5c264..1135562834a 100644
--- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
+++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
@@ -1,5 +1,4 @@
<script>
-import Vue from 'vue';
import {
GlIcon,
GlFormInput,
@@ -8,11 +7,15 @@ import {
GlSearchBoxByType,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
+import { cloneDeep } from 'lodash';
+import Vue from 'vue';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { s__, __ } from '~/locale';
-// Mocks will be removed when integrating with BE is ready
-// data format is defined and will be the same as mocked (maybe with some minor changes)
-// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171
-import gitlabFieldsMock from './mocks/gitlabFields.json';
+import {
+ getMappingData,
+ getPayloadFields,
+ transformForSave,
+} from '../utils/mapping_transformations';
export const i18n = {
columns: {
@@ -40,18 +43,25 @@ export default {
directives: {
GlTooltip,
},
- inject: {
- gitlabAlertFields: {
- default: gitlabFieldsMock,
- },
- },
props: {
- payloadFields: {
+ alertFields: {
+ type: Array,
+ required: true,
+ validator: (fields) => {
+ return (
+ fields.length &&
+ fields.every(({ name, types, label }) => {
+ return typeof name === 'string' && Array.isArray(types) && typeof label === 'string';
+ })
+ );
+ },
+ },
+ parsedPayload: {
type: Array,
required: false,
default: () => [],
},
- mapping: {
+ savedMapping: {
type: Array,
required: false,
default: () => [],
@@ -59,31 +69,18 @@ export default {
},
data() {
return {
- gitlabFields: this.gitlabAlertFields,
+ gitlabFields: cloneDeep(this.alertFields),
};
},
computed: {
+ payloadFields() {
+ return getPayloadFields(this.parsedPayload);
+ },
mappingData() {
- return this.gitlabFields.map((gitlabField) => {
- const mappingFields = this.payloadFields.filter(({ type }) =>
- type.some((t) => gitlabField.compatibleTypes.includes(t)),
- );
-
- const foundMapping = this.mapping.find(
- ({ alertFieldName }) => alertFieldName === gitlabField.name,
- );
-
- const { fallbackAlertPaths, payloadAlertPaths } = foundMapping || {};
-
- return {
- mapping: payloadAlertPaths,
- fallback: fallbackAlertPaths,
- searchTerm: '',
- fallbackSearchTerm: '',
- mappingFields,
- ...gitlabField,
- };
- });
+ return getMappingData(this.gitlabFields, this.payloadFields, this.savedMapping);
+ },
+ hasFallbackColumn() {
+ return this.gitlabFields.some(({ numberOfFallbacks }) => Boolean(numberOfFallbacks));
},
},
methods: {
@@ -91,6 +88,7 @@ export default {
const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey);
const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [valueKey]: mappingKey } };
Vue.set(this.gitlabFields, fieldIndex, updatedField);
+ this.$emit('onMappingUpdate', transformForSave(this.mappingData));
},
setSearchTerm(search = '', searchFieldKey, gitlabKey) {
const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey);
@@ -99,7 +97,6 @@ export default {
},
filterFields(searchTerm = '', fields) {
const search = searchTerm.toLowerCase();
-
return fields.filter((field) => field.label.toLowerCase().includes(search));
},
isSelected(fieldValue, mapping) {
@@ -111,8 +108,10 @@ export default {
this.$options.i18n.makeSelection
);
},
- getFieldValue({ label, type }) {
- return `${label} (${type.join(__(' or '))})`;
+ getFieldValue({ label, types }) {
+ const type = types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(__(' or '));
+
+ return `${label} (${type})`;
},
noResults(searchTerm, fields) {
return !this.filterFields(searchTerm, fields).length;
@@ -131,7 +130,11 @@ export default {
<h5 id="parsedFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3">
{{ $options.i18n.columns.payloadKeyTitle }}
</h5>
- <h5 id="fallbackFieldsHeader" class="gl-display-table-cell gl-py-3 gl-pr-3">
+ <h5
+ v-if="hasFallbackColumn"
+ id="fallbackFieldsHeader"
+ class="gl-display-table-cell gl-py-3 gl-pr-3"
+ >
{{ $options.i18n.columns.fallbackKeyTitle }}
<gl-icon
v-gl-tooltip
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index 1ae7f826ce6..18372c54b84 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -12,18 +12,18 @@ import {
GlModalDirective,
GlToggle,
} from '@gitlab/ui';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import MappingBuilder from './alert_mapping_builder.vue';
-import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue';
-import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
integrationTypes,
JSON_VALIDATE_DELAY,
targetPrometheusUrlPlaceholder,
typeSet,
} from '../constants';
+import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
+import MappingBuilder from './alert_mapping_builder.vue';
+import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue';
// Mocks will be removed when integrating with BE is ready
// data format is defined and will be the same as mocked (maybe with some minor changes)
// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171
@@ -125,6 +125,9 @@ export default {
prometheus: {
default: {},
},
+ multiIntegrations: {
+ default: false,
+ },
},
props: {
loading: {
@@ -135,6 +138,11 @@ export default {
type: Boolean,
required: true,
},
+ alertFields: {
+ type: Array,
+ required: false,
+ default: null,
+ },
},
apollo: {
currentIntegration: {
@@ -152,6 +160,7 @@ export default {
},
resetSamplePayloadConfirmed: false,
customMapping: null,
+ mapping: [],
parsingPayload: false,
currentIntegration: null,
};
@@ -195,14 +204,16 @@ export default {
},
showMappingBuilder() {
return (
+ this.multiIntegrations &&
this.glFeatures.multipleHttpIntegrationsCustomMapping &&
- this.selectedIntegration === typeSet.http
+ this.selectedIntegration === typeSet.http &&
+ this.alertFields?.length
);
},
- mappingBuilderFields() {
+ parsedSamplePayload() {
return this.customMapping?.samplePayload?.payloadAlerFields?.nodes;
},
- mappingBuilderMapping() {
+ savedMapping() {
return this.customMapping?.storedMapping?.nodes;
},
hasSamplePayload() {
@@ -255,9 +266,20 @@ export default {
},
submit() {
const { name, apiUrl } = this.integrationForm;
+ const customMappingVariables = this.glFeatures.multipleHttpIntegrationsCustomMapping
+ ? {
+ payloadAttributeMappings: this.mapping,
+ payloadExample: this.integrationTestPayload.json,
+ }
+ : {};
+
const variables =
this.selectedIntegration === typeSet.http
- ? { name, active: this.active }
+ ? {
+ name,
+ active: this.active,
+ ...customMappingVariables,
+ }
: { apiUrl, active: this.active };
const integrationPayload = { type: this.selectedIntegration, variables };
@@ -336,6 +358,9 @@ export default {
this.integrationTestPayload.json = res?.samplePayload.body;
});
},
+ updateMapping(mapping) {
+ this.mapping = mapping;
+ },
},
};
</script>
@@ -541,8 +566,10 @@ export default {
>
<span>{{ $options.i18n.integrationFormSteps.step5.intro }}</span>
<mapping-builder
- :payload-fields="mappingBuilderFields"
- :mapping="mappingBuilderMapping"
+ :parsed-payload="parsedSamplePayload"
+ :saved-mapping="savedMapping"
+ :alert-fields="alertFields"
+ @onMappingUpdate="updateMapping"
/>
</gl-form-group>
</div>
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index d0cac066ffa..366f2209fb2 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -1,21 +1,19 @@
<script>
-import { s__ } from '~/locale';
-import { fetchPolicies } from '~/lib/graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
-import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql';
-import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
+import { fetchPolicies } from '~/lib/graphql';
+import { s__ } from '~/locale';
+import { typeSet } from '../constants';
import createHttpIntegrationMutation from '../graphql/mutations/create_http_integration.mutation.graphql';
import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql';
-import updateHttpIntegrationMutation from '../graphql/mutations/update_http_integration.mutation.graphql';
-import updatePrometheusIntegrationMutation from '../graphql/mutations/update_prometheus_integration.mutation.graphql';
import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql';
import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql';
import updateCurrentIntergrationMutation from '../graphql/mutations/update_current_intergration.mutation.graphql';
-import IntegrationsList from './alerts_integrations_list.vue';
-import AlertSettingsForm from './alerts_settings_form.vue';
+import updateHttpIntegrationMutation from '../graphql/mutations/update_http_integration.mutation.graphql';
+import updatePrometheusIntegrationMutation from '../graphql/mutations/update_prometheus_integration.mutation.graphql';
+import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
+import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql';
import service from '../services';
-import { typeSet } from '../constants';
import {
updateStoreAfterIntegrationDelete,
updateStoreAfterIntegrationAdd,
@@ -27,6 +25,8 @@ import {
UPDATE_INTEGRATION_ERROR,
INTEGRATION_PAYLOAD_TEST_ERROR,
} from '../utils/error_messages';
+import IntegrationsList from './alerts_integrations_list.vue';
+import AlertSettingsForm from './alerts_settings_form.vue';
export default {
typeSet,
@@ -57,6 +57,13 @@ export default {
default: false,
},
},
+ props: {
+ alertFields: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ },
apollo: {
integrations: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
@@ -312,6 +319,7 @@ export default {
<alert-settings-form
:loading="isUpdating"
:can-add-integration="canAddIntegration"
+ :alert-fields="alertFields"
@create-new-integration="createNewIntegration"
@update-integration="updateIntegration"
@reset-token="resetToken"
diff --git a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json b/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json
deleted file mode 100644
index ac559a30eda..00000000000
--- a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json
+++ /dev/null
@@ -1,112 +0,0 @@
-[
- {
- "name": "title",
- "label": "Title",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ],
- "numberOfFallbacks": 1
- },
- {
- "name": "description",
- "label": "Description",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "startTime",
- "label": "Start time",
- "type": [
- "DateTime"
- ],
- "compatibleTypes": [
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "service",
- "label": "Service",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "monitoringTool",
- "label": "Monitoring tool",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "hosts",
- "label": "Hosts",
- "type": [
- "String",
- "Array"
- ],
- "compatibleTypes": [
- "String",
- "Array",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "severity",
- "label": "Severity",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "fingerprint",
- "label": "Fingerprint",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- },
- {
- "name": "environment",
- "label": "Environment",
- "type": [
- "String"
- ],
- "compatibleTypes": [
- "String",
- "Number",
- "DateTime"
- ]
- }
-]
diff --git a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json
index 5326678155d..80fbebf2a60 100644
--- a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json
+++ b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json
@@ -4,95 +4,69 @@
"payloadAlerFields": {
"nodes": [
{
- "name": "dashboardId",
+ "path": ["dashboardId"],
"label": "Dashboard Id",
- "type": [
- "Number"
- ]
+ "type": "string"
},
{
- "name": "evalMatches",
+ "path": ["evalMatches"],
"label": "Eval Matches",
- "type": [
- "Array"
- ]
+ "type": "array"
},
{
- "name": "createdAt",
+ "path": ["createdAt"],
"label": "Created At",
- "type": [
- "DateTime"
- ]
+ "type": "datetime"
},
{
- "name": "imageUrl",
+ "path": ["imageUrl"],
"label": "Image Url",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "message",
+ "path": ["message"],
"label": "Message",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "orgId",
+ "path": ["orgId"],
"label": "Org Id",
- "type": [
- "Number"
- ]
+ "type": "string"
},
{
- "name": "panelId",
+ "path": ["panelId"],
"label": "Panel Id",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "ruleId",
+ "path": ["ruleId"],
"label": "Rule Id",
- "type": [
- "Number"
- ]
+ "type": "string"
},
{
- "name": "ruleName",
+ "path": ["ruleName"],
"label": "Rule Name",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "ruleUrl",
+ "path": ["ruleUrl"],
"label": "Rule Url",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "state",
+ "path": ["state"],
"label": "State",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "title",
+ "path": ["title"],
"label": "Title",
- "type": [
- "String"
- ]
+ "type": "string"
},
{
- "name": "tags",
+ "path": ["tags", "tag"],
"label": "Tags",
- "type": [
- "Object"
- ]
+ "type": "string"
}
]
}
diff --git a/app/assets/javascripts/alerts_settings/graphql.js b/app/assets/javascripts/alerts_settings/graphql.js
index 3dbfa69a8e9..5fd05169533 100644
--- a/app/assets/javascripts/alerts_settings/graphql.js
+++ b/app/assets/javascripts/alerts_settings/graphql.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import produce from 'immer';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import getCurrentIntegrationQuery from './graphql/queries/get_current_integration.query.graphql';
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
index d1dacbad40a..f3fc10b4bd4 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
@@ -1,7 +1,21 @@
#import "../fragments/integration_item.fragment.graphql"
-mutation createHttpIntegration($projectPath: ID!, $name: String!, $active: Boolean!) {
- httpIntegrationCreate(input: { projectPath: $projectPath, name: $name, active: $active }) {
+mutation createHttpIntegration(
+ $projectPath: ID!
+ $name: String!
+ $active: Boolean!
+ $payloadExample: JsonString
+ $payloadAttributeMappings: [AlertManagementPayloadAlertFieldInput!]
+) {
+ httpIntegrationCreate(
+ input: {
+ projectPath: $projectPath
+ name: $name
+ active: $active
+ payloadExample: $payloadExample
+ payloadAttributeMappings: $payloadAttributeMappings
+ }
+ ) {
errors
integration {
...IntegrationItem
diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js
index 85858956987..8506b3fda01 100644
--- a/app/assets/javascripts/alerts_settings/index.js
+++ b/app/assets/javascripts/alerts_settings/index.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
+import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import AlertSettingsWrapper from './components/alerts_settings_wrapper.vue';
import apolloProvider from './graphql';
@@ -31,6 +31,7 @@ export default (el) => {
url,
projectPath,
multiIntegrations,
+ alertFields,
} = el.dataset;
return new Vue({
@@ -60,7 +61,14 @@ export default (el) => {
},
apolloProvider,
render(createElement) {
- return createElement('alert-settings-wrapper');
+ return createElement('alert-settings-wrapper', {
+ props: {
+ alertFields:
+ gon.features?.multipleHttpIntegrationsCustomMapping && parseBoolean(multiIntegrations)
+ ? JSON.parse(alertFields)
+ : null,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js
new file mode 100644
index 00000000000..a86103540c0
--- /dev/null
+++ b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js
@@ -0,0 +1,61 @@
+/**
+ * Given data for GitLab alert fields, parsed payload fields data and previously stored mapping (if any)
+ * creates an object in a form convenient to build UI && interact with it
+ * @param {Object} gitlabFields - structure describing GitLab alert fields
+ * @param {Object} payloadFields - parsed from sample JSON sample alert fields
+ * @param {Object} savedMapping - GitLab fields to parsed fields mapping
+ *
+ * @return {Object} mapping data for UI mapping builder
+ */
+export const getMappingData = (gitlabFields, payloadFields, savedMapping) => {
+ return gitlabFields.map((gitlabField) => {
+ // find fields from payload that match gitlab alert field by type
+ const mappingFields = payloadFields.filter(({ type }) => gitlabField.types.includes(type));
+
+ // find the mapping that was previously stored
+ const foundMapping = savedMapping.find(({ fieldName }) => fieldName === gitlabField.name);
+
+ const { fallbackAlertPaths, payloadAlertPaths } = foundMapping || {};
+
+ return {
+ mapping: payloadAlertPaths,
+ fallback: fallbackAlertPaths,
+ searchTerm: '',
+ fallbackSearchTerm: '',
+ mappingFields,
+ ...gitlabField,
+ };
+ });
+};
+
+/**
+ * Based on mapping data configured by the user creates an object in a format suitable for save on BE
+ * @param {Object} mappingData - structure describing mapping between GitLab fields and parsed payload fields
+ *
+ * @return {Object} mapping data to send to BE
+ */
+export const transformForSave = (mappingData) => {
+ return mappingData.reduce((acc, field) => {
+ const mapped = field.mappingFields.find(({ name }) => name === field.mapping);
+ if (mapped) {
+ const { path, type, label } = mapped;
+ acc.push({
+ fieldName: field.name.toUpperCase(),
+ path,
+ type: type.toUpperCase(),
+ label,
+ });
+ }
+ return acc;
+ }, []);
+};
+
+/**
+ * Adds `name` prop to each provided by BE parsed payload field
+ * @param {Object} payload - parsed sample payload
+ *
+ * @return {Object} same as input with an extra `name` property which basically serves as a key to make a match
+ */
+export const getPayloadFields = (payload) => {
+ return payload.map((field) => ({ ...field, name: field.path.join('_') }));
+};
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/app.vue b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
index 8df4d2e2524..3bf41eaa008 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/app.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue
@@ -1,10 +1,10 @@
<script>
+import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants';
+import ChartsConfig from './charts_config';
import InstanceCounts from './instance_counts.vue';
import InstanceStatisticsCountChart from './instance_statistics_count_chart.vue';
-import UsersChart from './users_chart.vue';
import ProjectsAndGroupsChart from './projects_and_groups_chart.vue';
-import ChartsConfig from './charts_config';
-import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants';
+import UsersChart from './users_chart.vue';
export default {
name: 'InstanceStatisticsApp',
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue b/app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue
index b331e659a92..f3779ed62e9 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/instance_counts.vue
@@ -1,9 +1,9 @@
<script>
-import * as Sentry from '~/sentry/wrapper';
-import { s__ } from '~/locale';
+import MetricCard from '~/analytics/shared/components/metric_card.vue';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
-import MetricCard from '~/analytics/shared/components/metric_card.vue';
+import { s__ } from '~/locale';
+import * as Sentry from '~/sentry/wrapper';
import instanceStatisticsCountQuery from '../graphql/queries/instance_statistics_count.query.graphql';
const defaultPrecision = 0;
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue
index 620f38bd50f..e2defe0572d 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue
@@ -1,16 +1,16 @@
<script>
-import { GlLineChart } from '@gitlab/ui/dist/charts';
import { GlAlert } from '@gitlab/ui';
+import { GlLineChart } from '@gitlab/ui/dist/charts';
import { some, every } from 'lodash';
-import * as Sentry from '~/sentry/wrapper';
-import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import {
differenceInMonths,
formatDateAsMonth,
getDayDifference,
} from '~/lib/utils/datetime_utility';
-import { getAverageByMonth, getEarliestDate, generateDataKeys } from '../utils';
+import * as Sentry from '~/sentry/wrapper';
+import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { TODAY, START_DATE } from '../constants';
+import { getAverageByMonth, getEarliestDate, generateDataKeys } from '../utils';
const QUERY_DATA_KEY = 'instanceStatisticsMeasurements';
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue
index 46cc05fc124..3ffec90fb68 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue
@@ -3,10 +3,10 @@ import { GlAlert } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import produce from 'immer';
import { sortBy } from 'lodash';
+import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
+import { s__, __ } from '~/locale';
import * as Sentry from '~/sentry/wrapper';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
-import { s__, __ } from '~/locale';
-import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
import latestGroupsQuery from '../graphql/queries/groups.query.graphql';
import latestProjectsQuery from '../graphql/queries/projects.query.graphql';
import { getAverageByMonth } from '../utils';
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue
index 03462113630..73940f028a1 100644
--- a/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue
+++ b/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue
@@ -3,10 +3,10 @@ import { GlAlert } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import produce from 'immer';
import { sortBy } from 'lodash';
+import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
import * as Sentry from '~/sentry/wrapper';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
-import { __ } from '~/locale';
-import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
import usersQuery from '../graphql/queries/users.query.graphql';
import { getAverageByMonth } from '../utils';
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 0a3db8ad3a6..c7e6b98a934 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,7 +1,7 @@
-import axios from './lib/utils/axios_utils';
-import { joinPaths } from './lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
+import axios from './lib/utils/axios_utils';
+import { joinPaths } from './lib/utils/url_utility';
const DEFAULT_PER_PAGE = 20;
@@ -81,8 +81,10 @@ const Api = {
usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
- billableGroupMembersPath: '/api/:version/groups/:id/billable_members',
containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/',
+ projectNotificationSettingsPath: '/api/:version/projects/:id/notification_settings',
+ groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings',
+ notificationSettingsPath: '/api/:version/notification_settings',
group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -179,9 +181,9 @@ const Api = {
});
},
- groupLabels(namespace) {
+ groupLabels(namespace, options = {}) {
const url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespace);
- return axios.get(url).then(({ data }) => data);
+ return axios.get(url, options).then(({ data }) => data);
},
// Return namespaces list. Filtered by query
@@ -442,10 +444,11 @@ const Api = {
});
},
- applySuggestion(id, message) {
+ applySuggestion(id, message = '') {
const url = Api.buildUrl(Api.applySuggestionPath).replace(':id', encodeURIComponent(id));
+ const params = gon.features?.suggestionsCustomCommit ? { commit_message: message } : false;
- return axios.put(url, { commit_message: message });
+ return axios.put(url, params);
},
applySuggestionBatch(ids) {
@@ -879,31 +882,32 @@ const Api = {
return axios.delete(url);
},
- fetchBillableGroupMembersList(namespaceId, options = {}, callback = () => {}) {
- const url = Api.buildUrl(this.billableGroupMembersPath).replace(':id', namespaceId);
- const defaults = {
- per_page: DEFAULT_PER_PAGE,
- page: 1,
- };
+ async updateNotificationSettings(projectId, groupId, data = {}) {
+ let url = Api.buildUrl(this.notificationSettingsPath);
+
+ if (projectId) {
+ url = Api.buildUrl(this.projectNotificationSettingsPath).replace(':id', projectId);
+ } else if (groupId) {
+ url = Api.buildUrl(this.groupNotificationSettingsPath).replace(':id', groupId);
+ }
+
+ const result = await axios.put(url, data);
+
+ return result;
+ },
- const passedOptions = options;
+ async getNotificationSettings(projectId, groupId) {
+ let url = Api.buildUrl(this.notificationSettingsPath);
- // calling search API with empty string will not return results
- if (!passedOptions.search) {
- passedOptions.search = undefined;
+ if (projectId) {
+ url = Api.buildUrl(this.projectNotificationSettingsPath).replace(':id', projectId);
+ } else if (groupId) {
+ url = Api.buildUrl(this.groupNotificationSettingsPath).replace(':id', groupId);
}
- return axios
- .get(url, {
- params: {
- ...defaults,
- ...passedOptions,
- },
- })
- .then(({ data, headers }) => {
- callback(data);
- return { data, headers };
- });
+ const result = await axios.get(url);
+
+ return result;
},
};
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js
index e5983ec3c58..5efc7063efa 100644
--- a/app/assets/javascripts/api/user_api.js
+++ b/app/assets/javascripts/api/user_api.js
@@ -1,8 +1,8 @@
+import { deprecatedCreateFlash as flash } from '~/flash';
+import { __ } from '~/locale';
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
import { DEFAULT_PER_PAGE } from './constants';
-import { deprecatedCreateFlash as flash } from '~/flash';
-import { __ } from '~/locale';
const USER_COUNTS_PATH = '/api/:version/user_counts';
const USERS_PATH = '/api/:version/users.json';
diff --git a/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_application_setting.query.graphql b/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_application_setting.query.graphql
new file mode 100644
index 00000000000..9d833d81a3f
--- /dev/null
+++ b/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_application_setting.query.graphql
@@ -0,0 +1,5 @@
+query getKeepLatestArtifactApplicationSetting {
+ ciApplicationSettings {
+ keepLatestArtifact
+ }
+}
diff --git a/app/assets/javascripts/artifacts_settings/index.js b/app/assets/javascripts/artifacts_settings/index.js
index d99d2be81cf..531b42bc185 100644
--- a/app/assets/javascripts/artifacts_settings/index.js
+++ b/app/assets/javascripts/artifacts_settings/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
import KeepLatestArtifactCheckbox from '~/artifacts_settings/keep_latest_artifact_checkbox.vue';
+import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue b/app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue
index 5684033f3af..92f1cc8117a 100644
--- a/app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue
+++ b/app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue
@@ -1,13 +1,23 @@
<script>
import { GlAlert, GlFormCheckbox, GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
-import GetKeepLatestArtifactProjectSetting from './graphql/queries/get_keep_latest_artifact_project_setting.query.graphql';
import UpdateKeepLatestArtifactProjectSetting from './graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql';
-
-const FETCH_ERROR = __('There was a problem fetching the keep latest artifact setting.');
-const UPDATE_ERROR = __('There was a problem updating the keep latest artifact setting.');
+import GetKeepLatestArtifactApplicationSetting from './graphql/queries/get_keep_latest_artifact_application_setting.query.graphql';
+import GetKeepLatestArtifactProjectSetting from './graphql/queries/get_keep_latest_artifact_project_setting.query.graphql';
export default {
+ errors: {
+ fetchError: __('There was a problem fetching the keep latest artifacts setting.'),
+ updateError: __('There was a problem updating the keep latest artifacts setting.'),
+ },
+ i18n: {
+ enabledHelpText: __(
+ 'The latest artifacts created by jobs in the most recent successful pipeline will be stored.',
+ ),
+ disabledHelpText: __('This feature is disabled at the instance level.'),
+ helpLinkText: __('More information'),
+ checkboxText: __('Keep artifacts from most recent successful jobs'),
+ },
components: {
GlAlert,
GlFormCheckbox,
@@ -33,21 +43,33 @@ export default {
return data.project?.ciCdSettings?.keepLatestArtifact;
},
error() {
- this.reportError(FETCH_ERROR);
+ this.reportError(this.$options.errors.fetchError);
+ },
+ },
+ projectSettingDisabled: {
+ query: GetKeepLatestArtifactApplicationSetting,
+ update(data) {
+ return !data.ciApplicationSettings?.keepLatestArtifact;
},
},
},
data() {
return {
- keepLatestArtifact: true,
+ keepLatestArtifact: null,
errorMessage: '',
isAlertDismissed: false,
+ projectSettingDisabled: true,
};
},
computed: {
shouldShowAlert() {
return this.errorMessage && !this.isAlertDismissed;
},
+ helpText() {
+ return this.projectSettingDisabled
+ ? this.$options.i18n.disabledHelpText
+ : this.$options.i18n.enabledHelpText;
+ },
},
methods: {
reportError(error) {
@@ -65,10 +87,10 @@ export default {
});
if (data.ciCdSettingsUpdate.errors.length) {
- this.reportError(UPDATE_ERROR);
+ this.reportError(this.$options.errors.updateError);
}
} catch (error) {
- this.reportError(UPDATE_ERROR);
+ this.reportError(this.$options.errors.updateError);
}
},
},
@@ -84,16 +106,13 @@ export default {
@dismiss="isAlertDismissed = true"
>{{ errorMessage }}</gl-alert
>
- <gl-form-checkbox v-model="keepLatestArtifact" @change="updateSetting"
- ><b class="gl-mr-3">{{ __('Keep artifacts from most recent successful jobs') }}</b>
- <gl-link :href="helpPagePath">{{ __('More information') }}</gl-link></gl-form-checkbox
- >
- <p>
- {{
- __(
- 'The latest artifacts created by jobs in the most recent successful pipeline will be stored.',
- )
- }}
- </p>
+ <gl-form-checkbox
+ v-model="keepLatestArtifact"
+ :disabled="projectSettingDisabled"
+ @change="updateSetting"
+ ><strong class="gl-mr-3">{{ $options.i18n.checkboxText }}</strong>
+ <gl-link :href="helpPagePath">{{ $options.i18n.helpLinkText }}</gl-link>
+ <template v-if="!$apollo.loading" #help>{{ helpText }}</template>
+ </gl-form-checkbox>
</div>
</template>
diff --git a/app/assets/javascripts/authentication/mount_2fa.js b/app/assets/javascripts/authentication/mount_2fa.js
index 6dead2f03db..52ed67b8c7b 100644
--- a/app/assets/javascripts/authentication/mount_2fa.js
+++ b/app/assets/javascripts/authentication/mount_2fa.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import initU2F from './u2f';
-import initWebauthn from './webauthn';
import U2FRegister from './u2f/register';
+import initWebauthn from './webauthn';
import WebAuthnRegister from './webauthn/register';
export const mount2faAuthentication = () => {
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
index 87502db8b82..0e589d98668 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
@@ -1,9 +1,9 @@
<script>
-import Mousetrap from 'mousetrap';
import { GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Tracking from '~/tracking';
+import Mousetrap from 'mousetrap';
import { __ } from '~/locale';
+import Tracking from '~/tracking';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
COPY_BUTTON_ACTION,
DOWNLOAD_BUTTON_ACTION,
diff --git a/app/assets/javascripts/authentication/u2f/authenticate.js b/app/assets/javascripts/authentication/u2f/authenticate.js
index f5217e9c9be..22eca904f32 100644
--- a/app/assets/javascripts/authentication/u2f/authenticate.js
+++ b/app/assets/javascripts/authentication/u2f/authenticate.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { template as lodashTemplate, omit } from 'lodash';
-import importU2FLibrary from './util';
import U2FError from './error';
+import importU2FLibrary from './util';
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
//
diff --git a/app/assets/javascripts/authentication/u2f/register.js b/app/assets/javascripts/authentication/u2f/register.js
index 52940e1c305..6c98f0978bc 100644
--- a/app/assets/javascripts/authentication/u2f/register.js
+++ b/app/assets/javascripts/authentication/u2f/register.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { template as lodashTemplate } from 'lodash';
import { __ } from '~/locale';
-import importU2FLibrary from './util';
import U2FError from './error';
+import importU2FLibrary from './util';
// Register U2F (universal 2nd factor) devices for users to authenticate with.
//
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 22717a3f84c..6fb90551ed7 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,15 +1,15 @@
/* eslint-disable class-methods-use-this, @gitlab/require-i18n-strings */
-import $ from 'jquery';
-import { uniq } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import $ from 'jquery';
import Cookies from 'js-cookie';
-import { __ } from './locale';
-import { isInVueNoteablePage } from './lib/utils/dom_utils';
-import { deprecatedCreateFlash as flash } from './flash';
-import axios from './lib/utils/axios_utils';
+import { uniq } from 'lodash';
import * as Emoji from '~/emoji';
import { dispose, fixTitle } from '~/tooltips';
+import { deprecatedCreateFlash as flash } from './flash';
+import axios from './lib/utils/axios_utils';
+import { isInVueNoteablePage } from './lib/utils/dom_utils';
+import { __ } from './locale';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
@@ -560,7 +560,7 @@ export class AwardsHandler {
}
findMatchingEmojiElements(query) {
- const emojiMatches = this.emoji.searchEmoji(query, { match: 'fuzzy' }).map(({ name }) => name);
+ const emojiMatches = this.emoji.searchEmoji(query).map((x) => x.emoji.name);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements.filter(
(i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 9e09f527a39..20541ad8ccc 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import { escape, debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
-import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
import createEmptyBadge from '../empty_badge';
@@ -179,7 +179,7 @@ export default {
id="badge-link-url"
v-model="linkUrl"
type="URL"
- class="form-control"
+ class="form-control gl-form-input"
required
@input="debouncedPreview"
/>
@@ -194,7 +194,7 @@ export default {
id="badge-image-url"
v-model="imageUrl"
type="URL"
- class="form-control"
+ class="form-control gl-form-input"
required
@input="debouncedPreview"
/>
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index 04c2d4a7493..f16a547e441 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -1,8 +1,8 @@
<script>
-import { mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
-import BadgeListRow from './badge_list_row.vue';
+import { mapState } from 'vuex';
import { GROUP_BADGE } from '../constants';
+import BadgeListRow from './badge_list_row.vue';
export default {
name: 'BadgeList',
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index 4ca91e7a589..fda51c98e2c 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon, GlButton, GlModalDirective } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
index 73c63a72b1c..8d5bd216367 100644
--- a/app/assets/javascripts/badges/components/badge_settings.vue
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlSprintf, GlModal } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
import Badge from './badge.vue';
diff --git a/app/assets/javascripts/badges/store/actions.js b/app/assets/javascripts/badges/store/actions.js
index 3377f6c0996..17a03564432 100644
--- a/app/assets/javascripts/badges/store/actions.js
+++ b/app/assets/javascripts/badges/store/actions.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
-import types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import types from './mutation_types';
export const transformBackendBadge = (badge) => ({
...convertObjectPropsToCamelCase(badge, true),
diff --git a/app/assets/javascripts/badges/store/index.js b/app/assets/javascripts/badges/store/index.js
index 7a5df403a0e..848deb2baa7 100644
--- a/app/assets/javascripts/badges/store/index.js
+++ b/app/assets/javascripts/badges/store/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import createState from './state';
import actions from './actions';
import mutations from './mutations';
+import createState from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/badges/store/mutations.js b/app/assets/javascripts/badges/store/mutations.js
index 3f4689aeb17..9e27af21ed6 100644
--- a/app/assets/javascripts/badges/store/mutations.js
+++ b/app/assets/javascripts/badges/store/mutations.js
@@ -1,5 +1,5 @@
-import types from './mutation_types';
import { PROJECT_BADGE } from '../constants';
+import types from './mutation_types';
const reorderBadges = (badges) =>
badges.sort((a, b) => {
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index 74069b61f07..e6de724512f 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -1,10 +1,9 @@
<script>
/* eslint-disable vue/no-v-html */
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
+import { mapActions, mapGetters, mapState } from 'vuex';
import NoteableNote from '~/notes/components/noteable_note.vue';
import PublishButton from './publish_button.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -12,7 +11,6 @@ export default {
PublishButton,
GlButton,
},
- mixins: [glFeatureFlagsMixin()],
props: {
draft: {
type: Object,
@@ -63,14 +61,14 @@ export default {
this.isEditingDraft = false;
},
handleMouseEnter(draft) {
- if (this.glFeatures.multilineComments && draft.position) {
+ if (draft.position) {
this.setSelectedCommentPositionHover(draft.position.line_range);
}
},
handleMouseLeave(draft) {
- // Even though position isn't used here we still don't want to unecessarily call a mutation
+ // Even though position isn't used here we still don't want to unnecessarily call a mutation
// The lack of position tells us that highlighting is irrelevant in this context
- if (this.glFeatures.multilineComments && draft.position) {
+ if (draft.position) {
this.setSelectedCommentPositionHover();
}
},
@@ -79,7 +77,6 @@ export default {
</script>
<template>
<article
- role="article"
class="draft-note-component note-wrapper"
@mouseenter="handleMouseEnter(draft)"
@mouseleave="handleMouseLeave(draft)"
diff --git a/app/assets/javascripts/batch_comments/components/drafts_count.vue b/app/assets/javascripts/batch_comments/components/drafts_count.vue
index 7a8482ac341..5e110b101eb 100644
--- a/app/assets/javascripts/batch_comments/components/drafts_count.vue
+++ b/app/assets/javascripts/batch_comments/components/drafts_count.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlBadge } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
index e18dc344cd7..fb643d441ec 100644
--- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { mapActions, mapGetters } from 'vuex';
import PreviewItem from './preview_item.vue';
export default {
diff --git a/app/assets/javascripts/batch_comments/components/preview_item.vue b/app/assets/javascripts/batch_comments/components/preview_item.vue
index 3e93168f0e2..756bcfdb3d0 100644
--- a/app/assets/javascripts/batch_comments/components/preview_item.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_item.vue
@@ -1,22 +1,21 @@
<script>
-import { mapGetters } from 'vuex';
import { GlSprintf, GlIcon } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { sprintf, __ } from '~/locale';
-import resolvedStatusMixin from '../mixins/resolved_status';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
getStartLineNumber,
getEndLineNumber,
getLineClasses,
} from '~/notes/components/multiline_comment_utils';
+import resolvedStatusMixin from '../mixins/resolved_status';
export default {
components: {
GlIcon,
GlSprintf,
},
- mixins: [resolvedStatusMixin, glFeatureFlagsMixin()],
+ mixins: [resolvedStatusMixin],
props: {
draft: {
type: Object,
@@ -71,6 +70,10 @@ export default {
return this.draft.position || this.discussion.position;
},
startLineNumber() {
+ if (this.position?.position_type === IMAGE_DIFF_POSITION_TYPE) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `${this.position.x}x ${this.position.y}y`;
+ }
return getStartLineNumber(this.position?.line_range);
},
endLineNumber() {
@@ -90,16 +93,12 @@ export default {
<span>
<span class="review-preview-item-header">
<gl-icon class="flex-shrink-0" :name="iconName" />
- <span
- class="bold text-nowrap"
- :class="{ 'gl-align-items-center': glFeatures.multilineComments }"
- >
+ <span class="bold text-nowrap gl-align-items-center">
<span class="review-preview-item-header-text block-truncated">
{{ titleText }}
</span>
<template v-if="showLinePosition">
- <template v-if="!glFeatures.multilineComments">:{{ linePosition }}</template>
- <template v-else-if="startLineNumber === endLineNumber">
+ <template v-if="startLineNumber === endLineNumber">
:<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
</template>
<gl-sprintf v-else :message="__(':%{startLine} to %{endLine}')">
diff --git a/app/assets/javascripts/batch_comments/components/publish_button.vue b/app/assets/javascripts/batch_comments/components/publish_button.vue
index ecced36771e..8568dba5947 100644
--- a/app/assets/javascripts/batch_comments/components/publish_button.vue
+++ b/app/assets/javascripts/batch_comments/components/publish_button.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import DraftsCount from './drafts_count.vue';
export default {
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
index a29409c52ae..36fef06eeff 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
@@ -1,9 +1,9 @@
import { deprecatedCreateFlash as flash } from '~/flash';
-import { __ } from '~/locale';
import { scrollToElement } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import { CHANGES_TAB, DISCUSSION_TAB, SHOW_TAB } from '../../../constants';
import service from '../../../services/drafts_service';
import * as types from './mutation_types';
-import { CHANGES_TAB, DISCUSSION_TAB, SHOW_TAB } from '../../../constants';
export const saveDraft = ({ dispatch }, draft) =>
dispatch('saveNote', { ...draft, isDraft: true }, { root: true });
@@ -67,13 +67,23 @@ export const publishReview = ({ commit, dispatch, getters }) => {
.catch(() => commit(types.RECEIVE_PUBLISH_REVIEW_ERROR));
};
-export const updateDiscussionsAfterPublish = ({ dispatch, getters, rootGetters }) =>
- dispatch('fetchDiscussions', { path: getters.getNotesData.discussionsPath }, { root: true }).then(
- () =>
- dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, {
- root: true,
- }),
- );
+export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGetters }) => {
+ if (window.gon?.features?.paginatedNotes) {
+ await dispatch('stopPolling', null, { root: true });
+ await dispatch('fetchData', null, { root: true });
+ await dispatch('restartPolling', null, { root: true });
+ } else {
+ await dispatch(
+ 'fetchDiscussions',
+ { path: getters.getNotesData.discussionsPath },
+ { root: true },
+ );
+ }
+
+ dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, {
+ root: true,
+ });
+};
export const updateDraft = (
{ commit, getters },
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/index.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/index.js
index 81dab0566c1..81a51b5ab31 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/index.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/index.js
@@ -1,7 +1,7 @@
-import state from './state';
-import mutations from './mutations';
import * as actions from './actions';
import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
export default () => ({
namespaced: true,
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index a5404539c17..181d841a068 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,13 +1,11 @@
import Autosize from 'autosize';
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
-document.addEventListener('DOMContentLoaded', () => {
- waitForCSSLoaded(() => {
- const autosizeEls = document.querySelectorAll('.js-autosize');
+waitForCSSLoaded(() => {
+ const autosizeEls = document.querySelectorAll('.js-autosize');
- Autosize(autosizeEls);
- Autosize.update(autosizeEls);
+ Autosize(autosizeEls);
+ Autosize.update(autosizeEls);
- autosizeEls.forEach((el) => el.classList.add('js-autosize-initialized'));
- });
+ autosizeEls.forEach((el) => el.classList.add('js-autosize-initialized'));
});
diff --git a/app/assets/javascripts/behaviors/collapse_sidebar_on_window_resize.js b/app/assets/javascripts/behaviors/collapse_sidebar_on_window_resize.js
index c4af34b848b..5e474e56196 100644
--- a/app/assets/javascripts/behaviors/collapse_sidebar_on_window_resize.js
+++ b/app/assets/javascripts/behaviors/collapse_sidebar_on_window_resize.js
@@ -1,5 +1,5 @@
-import $ from 'jquery';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import $ from 'jquery';
/**
* This behavior collapses the right sidebar
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index 1176fa6628d..a31bcc2cb41 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -1,5 +1,5 @@
-import $ from 'jquery';
import Clipboard from 'clipboard';
+import $ from 'jquery';
import { sprintf, __ } from '~/locale';
import { fixTitle, show } from '~/tooltips';
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index 1fa37999d62..bf7a87144f9 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -1,6 +1,12 @@
import 'document-register-element';
+import {
+ initEmojiMap,
+ getEmojiInfo,
+ emojiFallbackImageSrc,
+ emojiImageTag,
+ FALLBACK_EMOJI_KEY,
+} from '../emoji';
import isEmojiUnicodeSupported from '../emoji/support';
-import { initEmojiMap, getEmojiInfo, emojiFallbackImageSrc, emojiImageTag } from '../emoji';
class GlEmoji extends HTMLElement {
connectedCallback() {
@@ -17,6 +23,10 @@ class GlEmoji extends HTMLElement {
if (emojiInfo) {
if (name !== emojiInfo.name) {
+ if (emojiInfo.name === FALLBACK_EMOJI_KEY && this.innerHTML) {
+ return; // When fallback emoji is used, but there is a <img> provided, use the <img> instead
+ }
+
({ name } = emojiInfo);
this.dataset.name = emojiInfo.name;
}
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 75659bbf685..bfd025e8dab 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -2,17 +2,17 @@ import $ from 'jquery';
import './autosize';
import './bind_in_out';
import './markdown/render_gfm';
-import initCopyAsGFM from './markdown/copy_as_gfm';
+import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resize';
import initCopyToClipboard from './copy_to_clipboard';
import installGlEmojiElement from './gl_emoji';
+import { loadStartupCSS } from './load_startup_css';
+import initCopyAsGFM from './markdown/copy_as_gfm';
import './quick_submit';
import './requires_input';
+import initSelect2Dropdowns from './select2';
import initPageShortcuts from './shortcuts';
import './toggler_behavior';
import './preview_markdown';
-import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resize';
-import initSelect2Dropdowns from './select2';
-import { loadStartupCSS } from './load_startup_css';
loadStartupCSS();
@@ -25,19 +25,17 @@ initPageShortcuts();
initCollapseSidebarOnWindowResize();
initSelect2Dropdowns();
-document.addEventListener('DOMContentLoaded', () => {
- window.requestIdleCallback(
- () => {
- // Check if we have to Load GFM Input
- const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)');
- if ($gfmInputs.length) {
- import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete')
- .then(({ default: initGFMInput }) => {
- initGFMInput($gfmInputs);
- })
- .catch(() => {});
- }
- },
- { timeout: 500 },
- );
-});
+window.requestIdleCallback(
+ () => {
+ // Check if we have to Load GFM Input
+ const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)');
+ if ($gfmInputs.length) {
+ import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete')
+ .then(({ default: initGFMInput }) => {
+ initGFMInput($gfmInputs);
+ })
+ .catch(() => {});
+ }
+ },
+ { timeout: 500 },
+);
diff --git a/app/assets/javascripts/behaviors/markdown/editor_extensions.js b/app/assets/javascripts/behaviors/markdown/editor_extensions.js
index 308e31e7047..b512e4dbc8b 100644
--- a/app/assets/javascripts/behaviors/markdown/editor_extensions.js
+++ b/app/assets/javascripts/behaviors/markdown/editor_extensions.js
@@ -1,52 +1,45 @@
-import Doc from './nodes/doc';
-import Paragraph from './nodes/paragraph';
-import Text from './nodes/text';
-
+import Bold from './marks/bold';
+import Code from './marks/code';
+import InlineDiff from './marks/inline_diff';
+import InlineHTML from './marks/inline_html';
+import Italic from './marks/italic';
+import Link from './marks/link';
+import MathMark from './marks/math';
+import Strike from './marks/strike';
+import Audio from './nodes/audio';
import Blockquote from './nodes/blockquote';
+import BulletList from './nodes/bullet_list';
import CodeBlock from './nodes/code_block';
+import DescriptionDetails from './nodes/description_details';
+import DescriptionList from './nodes/description_list';
+import DescriptionTerm from './nodes/description_term';
+import Details from './nodes/details';
+import Doc from './nodes/doc';
+
+import Emoji from './nodes/emoji';
import HardBreak from './nodes/hard_break';
import Heading from './nodes/heading';
import HorizontalRule from './nodes/horizontal_rule';
import Image from './nodes/image';
+import ListItem from './nodes/list_item';
+import OrderedList from './nodes/ordered_list';
+import OrderedTaskList from './nodes/ordered_task_list';
+import Paragraph from './nodes/paragraph';
+import Reference from './nodes/reference';
+import Summary from './nodes/summary';
import Table from './nodes/table';
-import TableHead from './nodes/table_head';
import TableBody from './nodes/table_body';
-import TableHeaderRow from './nodes/table_header_row';
-import TableRow from './nodes/table_row';
import TableCell from './nodes/table_cell';
-
-import Emoji from './nodes/emoji';
-import Reference from './nodes/reference';
-
+import TableHead from './nodes/table_head';
+import TableHeaderRow from './nodes/table_header_row';
import TableOfContents from './nodes/table_of_contents';
-import Video from './nodes/video';
-import Audio from './nodes/audio';
-
-import BulletList from './nodes/bullet_list';
-import OrderedList from './nodes/ordered_list';
-import ListItem from './nodes/list_item';
-
-import DescriptionList from './nodes/description_list';
-import DescriptionTerm from './nodes/description_term';
-import DescriptionDetails from './nodes/description_details';
+import TableRow from './nodes/table_row';
import TaskList from './nodes/task_list';
-import OrderedTaskList from './nodes/ordered_task_list';
import TaskListItem from './nodes/task_list_item';
-
-import Summary from './nodes/summary';
-import Details from './nodes/details';
-
-import Bold from './marks/bold';
-import Italic from './marks/italic';
-import Strike from './marks/strike';
-import InlineDiff from './marks/inline_diff';
-
-import Link from './marks/link';
-import Code from './marks/code';
-import MathMark from './marks/math';
-import InlineHTML from './marks/inline_html';
+import Text from './nodes/text';
+import Video from './nodes/video';
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform
// GitLab Flavored Markdown (GFM) to HTML.
diff --git a/app/assets/javascripts/behaviors/markdown/marks/bold.js b/app/assets/javascripts/behaviors/markdown/marks/bold.js
index b537954c1cb..d307edd9fd3 100644
--- a/app/assets/javascripts/behaviors/markdown/marks/bold.js
+++ b/app/assets/javascripts/behaviors/markdown/marks/bold.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Bold as BaseBold } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Bold as BaseBold } from 'tiptap-extensions';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class Bold extends BaseBold {
diff --git a/app/assets/javascripts/behaviors/markdown/marks/code.js b/app/assets/javascripts/behaviors/markdown/marks/code.js
index a760ee80dd0..ccfe2cf5b8d 100644
--- a/app/assets/javascripts/behaviors/markdown/marks/code.js
+++ b/app/assets/javascripts/behaviors/markdown/marks/code.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Code as BaseCode } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Code as BaseCode } from 'tiptap-extensions';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class Code extends BaseCode {
diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
index 556e6f7df1c..e957f81b774 100644
--- a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
+++ b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Mark } from 'tiptap';
import { escape } from 'lodash';
+import { Mark } from 'tiptap';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class InlineHTML extends Mark {
diff --git a/app/assets/javascripts/behaviors/markdown/marks/italic.js b/app/assets/javascripts/behaviors/markdown/marks/italic.js
index 44b35c97739..dbef10536ab 100644
--- a/app/assets/javascripts/behaviors/markdown/marks/italic.js
+++ b/app/assets/javascripts/behaviors/markdown/marks/italic.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Italic as BaseItalic } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Italic as BaseItalic } from 'tiptap-extensions';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class Italic extends BaseItalic {
diff --git a/app/assets/javascripts/behaviors/markdown/marks/link.js b/app/assets/javascripts/behaviors/markdown/marks/link.js
index 5c23d6a5ceb..1111c51805d 100644
--- a/app/assets/javascripts/behaviors/markdown/marks/link.js
+++ b/app/assets/javascripts/behaviors/markdown/marks/link.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Link as BaseLink } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Link as BaseLink } from 'tiptap-extensions';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class Link extends BaseLink {
diff --git a/app/assets/javascripts/behaviors/markdown/marks/math.js b/app/assets/javascripts/behaviors/markdown/marks/math.js
index 04441d5d710..382bf5c9b5b 100644
--- a/app/assets/javascripts/behaviors/markdown/marks/math.js
+++ b/app/assets/javascripts/behaviors/markdown/marks/math.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Mark } from 'tiptap';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Mark } from 'tiptap';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
// Transforms generated HTML back to GFM for Banzai::Filter::MathFilter
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js
index b0bc8f79643..bd5868e5524 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Blockquote as BaseBlockquote } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Blockquote as BaseBlockquote } from 'tiptap-extensions';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class Blockquote extends BaseBlockquote {
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js
index 3b0792e1af8..209e7239998 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { BulletList as BaseBulletList } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { BulletList as BaseBulletList } from 'tiptap-extensions';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class BulletList extends BaseBulletList {
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/heading.js b/app/assets/javascripts/behaviors/markdown/nodes/heading.js
index fec8608cf5d..708da053a2f 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/heading.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/heading.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Heading as BaseHeading } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Heading as BaseHeading } from 'tiptap-extensions';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class Heading extends BaseHeading {
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js
index 695c7160bde..47a24eae1e8 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { HorizontalRule as BaseHorizontalRule } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { HorizontalRule as BaseHorizontalRule } from 'tiptap-extensions';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class HorizontalRule extends BaseHorizontalRule {
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/image.js b/app/assets/javascripts/behaviors/markdown/nodes/image.js
index 76746528e72..ade5839d10b 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/image.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/image.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Image as BaseImage } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Image as BaseImage } from 'tiptap-extensions';
import { placeholderImage } from '~/lazy_loader';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js
index 4237637ed9a..0f56e89dca6 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/list_item.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { ListItem as BaseListItem } from 'tiptap-extensions';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { ListItem as BaseListItem } from 'tiptap-extensions';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class ListItem extends BaseListItem {
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
index dec3207b1bb..93d00f27868 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Node } from 'tiptap';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Node } from 'tiptap';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class Paragraph extends Node {
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/playable.js b/app/assets/javascripts/behaviors/markdown/nodes/playable.js
index 9cbd95a7bd8..33bb6e0c31c 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/playable.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/playable.js
@@ -1,8 +1,8 @@
/* eslint-disable class-methods-use-this */
/* eslint-disable @gitlab/require-i18n-strings */
-import { Node } from 'tiptap';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Node } from 'tiptap';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
/**
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
index 6e3c16f0a08..2cb2bb9e7fe 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import TableRow from './table_row';
import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
+import TableRow from './table_row';
const CENTER_ALIGN = 'center';
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/text.js b/app/assets/javascripts/behaviors/markdown/nodes/text.js
index 84838c14999..4eab10c9d98 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/text.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/text.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
-import { Node } from 'tiptap';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+import { Node } from 'tiptap';
export default class Text extends Node {
get name() {
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 5e9d80e1529..7934eac2f7e 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -1,10 +1,10 @@
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
+import initUserPopovers from '../../user_popovers';
+import highlightCurrentUser from './highlight_current_user';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
import renderMetrics from './render_metrics';
-import highlightCurrentUser from './highlight_current_user';
-import initUserPopovers from '../../user_popovers';
// Render GitLab flavoured Markdown
//
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index f34fec4d449..5479866c99a 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -1,6 +1,6 @@
import { deprecatedCreateFlash as flash } from '~/flash';
-import { s__, sprintf } from '~/locale';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
+import { s__, sprintf } from '~/locale';
// Renders math using KaTeX in any element with the
// `js-render-math` class
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index 479782a1f1f..0cb13815c7e 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -140,7 +140,7 @@ function renderMermaids($els) {
'Warning: Displaying this diagram might cause performance issues on this page.',
)}</div>
<div class="gl-alert-actions">
- <button class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md new-gl-button">Display</button>
+ <button class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md gl-button">Display</button>
</div>
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index def1c567cd5..5405819cfe0 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -1,8 +1,8 @@
/* eslint-disable func-names */
import $ from 'jquery';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
// MarkdownPreview
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 68e831252d6..12a2baed6e2 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import '../commons/bootstrap';
-import { isInIssuePage } from '../lib/utils/common_utils';
import { __ } from '~/locale';
import { add, show, hide } from '~/tooltips';
+import { isInIssuePage } from '../lib/utils/common_utils';
// Quick Submit behavior
//
diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js
index 2f1951c97f9..a34d5dcaef8 100644
--- a/app/assets/javascripts/behaviors/secret_values.js
+++ b/app/assets/javascripts/behaviors/secret_values.js
@@ -1,5 +1,5 @@
-import { n__ } from '../locale';
import { parseBoolean } from '../lib/utils/common_utils';
+import { n__ } from '../locale';
export default class SecretValues {
constructor({
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index 10832583783..0513e807ed6 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -1,6 +1,6 @@
import { flatten } from 'lodash';
-import { s__ } from '~/locale';
import AccessorUtilities from '~/lib/utils/accessor';
+import { s__ } from '~/locale';
import { shouldDisableShortcuts } from './shortcuts_toggle';
export const LOCAL_STORAGE_KEY = 'gl-keyboard-shortcuts-customizations';
@@ -29,6 +29,7 @@ export const customizations = parsedCustomizations;
// All available commands
export const TOGGLE_PERFORMANCE_BAR = 'globalShortcuts.togglePerformanceBar';
+export const TOGGLE_CANARY = 'globalShortcuts.toggleCanary';
/** All keybindings, grouped and ordered with descriptions */
export const keybindingGroups = [
@@ -42,6 +43,12 @@ export const keybindingGroups = [
// eslint-disable-next-line @gitlab/require-i18n-strings
defaultKeys: ['p b'],
},
+ {
+ description: s__('KeyboardShortcuts|Toggle GitLab Next'),
+ command: TOGGLE_CANARY,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ defaultKeys: ['g x'],
+ },
],
},
]
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 50d2399b312..e4ec68601e0 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -1,15 +1,14 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
+import { flatten } from 'lodash';
import Mousetrap from 'mousetrap';
import Vue from 'vue';
-import { flatten } from 'lodash';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import findAndFollowLink from '~/lib/utils/navigation_utility';
+import { refreshCurrentPage, visitUrl } from '~/lib/utils/url_utility';
+
+import { keysFor, TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY } from './keybindings';
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
-import ShortcutsToggle from './shortcuts_toggle.vue';
-import axios from '../../lib/utils/axios_utils';
-import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
-import findAndFollowLink from '../../lib/utils/navigation_utility';
-import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
-import { keysFor, TOGGLE_PERFORMANCE_BAR } from './keybindings';
const defaultStopCallback = Mousetrap.prototype.stopCallback;
Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
@@ -20,15 +19,6 @@ Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo
return defaultStopCallback.call(this, e, element, combo);
};
-function initToggleButton() {
- return new Vue({
- el: document.querySelector('.js-toggle-shortcuts'),
- render(createElement) {
- return createElement(ShortcutsToggle);
- },
- });
-}
-
/**
* The key used to save and fetch the local Mousetrap instance
* attached to a `<textarea>` element using `jQuery.data`
@@ -65,13 +55,15 @@ function getToolbarBtnToShortcutsMap($textarea) {
export default class Shortcuts {
constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this);
- this.enabledHelp = [];
+ this.helpModalElement = null;
+ this.helpModalVueInstance = null;
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('/', Shortcuts.focusSearch);
Mousetrap.bind('f', this.focusFilter.bind(this));
Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), Shortcuts.onTogglePerfBar);
+ Mousetrap.bind(keysFor(TOGGLE_CANARY), Shortcuts.onToggleCanary);
const findFileURL = document.body.dataset.findFile;
@@ -106,11 +98,33 @@ export default class Shortcuts {
}
onToggleHelp(e) {
- if (e.preventDefault) {
+ if (e?.preventDefault) {
e.preventDefault();
}
- Shortcuts.toggleHelp(this.enabledHelp);
+ if (this.helpModalElement && this.helpModalVueInstance) {
+ this.helpModalVueInstance.$destroy();
+ this.helpModalElement.remove();
+ this.helpModalElement = null;
+ this.helpModalVueInstance = null;
+ } else {
+ this.helpModalElement = document.createElement('div');
+ document.body.append(this.helpModalElement);
+
+ this.helpModalVueInstance = new Vue({
+ el: this.helpModalElement,
+ components: {
+ ShortcutsHelp: () => import('./shortcuts_help.vue'),
+ },
+ render: (createElement) => {
+ return createElement('shortcuts-help', {
+ on: {
+ hidden: this.onToggleHelp,
+ },
+ });
+ },
+ });
+ }
}
static onTogglePerfBar(e) {
@@ -124,6 +138,14 @@ export default class Shortcuts {
refreshCurrentPage();
}
+ static onToggleCanary(e) {
+ e.preventDefault();
+ const canaryCookieName = 'gitlab_canary';
+ const currentValue = parseBoolean(Cookies.get(canaryCookieName));
+ Cookies.set(canaryCookieName, (!currentValue).toString(), { expires: 365, path: '/' });
+ refreshCurrentPage();
+ }
+
static toggleMarkdownPreview(e) {
// Check if short-cut was triggered while in Write Mode
const $target = $(e.target);
@@ -135,34 +157,6 @@ export default class Shortcuts {
$(document).triggerHandler('markdown-preview:toggle', [e]);
}
- static toggleHelp(location) {
- const $modal = $('#modal-shortcuts');
-
- if ($modal.length) {
- $modal.modal('toggle');
- return null;
- }
-
- return axios
- .get(gon.shortcuts_path, {
- responseType: 'text',
- })
- .then(({ data }) => {
- $.globalEval(data, { nonce: getCspNonceValue() });
-
- if (location && location.length > 0) {
- const results = [];
- for (let i = 0, len = location.length; i < len; i += 1) {
- results.push($(location[i]).show());
- }
- return results;
- }
-
- return $('.js-more-help-button').remove();
- })
- .then(initToggleButton);
- }
-
focusFilter(e) {
if (!this.filterInput) {
this.filterInput = $('input[type=search]', '.nav-controls');
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
new file mode 100644
index 00000000000..1277dd0ed37
--- /dev/null
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
@@ -0,0 +1,525 @@
+<script>
+/* eslint-disable @gitlab/vue-require-i18n-strings */
+import { GlIcon, GlModal } from '@gitlab/ui';
+import ShortcutsToggle from './shortcuts_toggle.vue';
+
+export default {
+ components: {
+ GlIcon,
+ GlModal,
+ ShortcutsToggle,
+ },
+ computed: {
+ ctrlCharacter() {
+ return window.gl.client.isMac ? '⌘' : 'ctrl';
+ },
+ onDotCom() {
+ return window.gon.dot_com;
+ },
+ },
+};
+</script>
+<template>
+ <gl-modal
+ modal-id="keyboard-shortcut-modal"
+ size="lg"
+ data-testid="modal-shortcuts"
+ :visible="true"
+ :hide-footer="true"
+ @hidden="$emit('hidden')"
+ >
+ <template #modal-title>
+ <shortcuts-toggle />
+ </template>
+ <div class="row">
+ <div class="col-lg-4">
+ <table class="shortcut-mappings text-2">
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Global Shortcuts') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>?</kbd>
+ </td>
+ <td>{{ __('Toggle this dialog') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift p</kbd>
+ </td>
+ <td>{{ __('Go to your projects') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift g</kbd>
+ </td>
+ <td>{{ __('Go to your groups') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift a</kbd>
+ </td>
+ <td>{{ __('Go to the activity feed') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift l</kbd>
+ </td>
+ <td>{{ __('Go to the milestone list') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift s</kbd>
+ </td>
+ <td>{{ __('Go to your snippets') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>s</kbd>
+ /
+ <kbd>/</kbd>
+ </td>
+ <td>{{ __('Start search') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift i</kbd>
+ </td>
+ <td>{{ __('Go to your issues') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift m</kbd>
+ </td>
+ <td>{{ __('Go to your merge requests') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift t</kbd>
+ </td>
+ <td>{{ __('Go to your To-Do list') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>p</kbd>
+ <kbd>b</kbd>
+ </td>
+ <td>{{ __('Toggle the Performance Bar') }}</td>
+ </tr>
+ <tr v-if="onDotCom">
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>x</kbd>
+ </td>
+ <td>{{ __('Toggle GitLab Next') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Editing') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} shift p</kbd>
+ </td>
+ <td>{{ __('Toggle Markdown preview') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-up" />
+ </kbd>
+ </td>
+ <td>
+ {{ __('Edit your most recent comment in a thread (from an empty textarea)') }}
+ </td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Wiki') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>e</kbd>
+ </td>
+ <td>{{ __('Edit wiki page') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Repository Graph') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-left" />
+ </kbd>
+ /
+ <kbd>h</kbd>
+ </td>
+ <td>{{ __('Scroll left') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-right" />
+ </kbd>
+ /
+ <kbd>l</kbd>
+ </td>
+ <td>{{ __('Scroll right') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-up" />
+ </kbd>
+ /
+ <kbd>k</kbd>
+ </td>
+ <td>{{ __('Scroll up') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-down" />
+ </kbd>
+ /
+ <kbd>j</kbd>
+ </td>
+ <td>{{ __('Scroll down') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ shift
+ <gl-icon name="arrow-up" />
+ / k
+ </kbd>
+ </td>
+ <td>{{ __('Scroll to top') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ shift
+ <gl-icon name="arrow-down" />
+ / j
+ </kbd>
+ </td>
+ <td>{{ __('Scroll to bottom') }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="col-lg-4">
+ <table class="shortcut-mappings text-2">
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Project') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>p</kbd>
+ </td>
+ <td>{{ __("Go to the project's overview page") }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>v</kbd>
+ </td>
+ <td>{{ __("Go to the project's activity feed") }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>r</kbd>
+ </td>
+ <td>{{ __('Go to releases') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>f</kbd>
+ </td>
+ <td>{{ __('Go to files') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>t</kbd>
+ </td>
+ <td>{{ __('Go to find file') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>c</kbd>
+ </td>
+ <td>{{ __('Go to commits') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>n</kbd>
+ </td>
+ <td>{{ __('Go to repository graph') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>d</kbd>
+ </td>
+ <td>{{ __('Go to repository charts') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>i</kbd>
+ </td>
+ <td>{{ __('Go to issues') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>i</kbd>
+ </td>
+ <td>{{ __('New issue') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>b</kbd>
+ </td>
+ <td>{{ __('Go to issue boards') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>m</kbd>
+ </td>
+ <td>{{ __('Go to merge requests') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>j</kbd>
+ </td>
+ <td>{{ __('Go to jobs') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>l</kbd>
+ </td>
+ <td>{{ __('Go to metrics') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>e</kbd>
+ </td>
+ <td>{{ __('Go to environments') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>k</kbd>
+ </td>
+ <td>{{ __('Go to kubernetes') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>s</kbd>
+ </td>
+ <td>{{ __('Go to snippets') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>w</kbd>
+ </td>
+ <td>{{ __('Go to wiki') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Project Files') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-up" />
+ </kbd>
+ </td>
+ <td>{{ __('Move selection up') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-down" />
+ </kbd>
+ </td>
+ <td>{{ __('Move selection down') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>enter</kbd>
+ </td>
+ <td>{{ __('Open Selection') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>esc</kbd>
+ </td>
+ <td>{{ __('Go back (while searching for files)') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>y</kbd>
+ </td>
+ <td>{{ __('Go to file permalink (while viewing a file)') }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="col-lg-4">
+ <table class="shortcut-mappings text-2">
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Epics, Issues, and Merge Requests') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>r</kbd>
+ </td>
+ <td>{{ __('Comment/Reply (quoting selected text)') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>e</kbd>
+ </td>
+ <td>{{ __('Edit description') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>l</kbd>
+ </td>
+ <td>{{ __('Change label') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Issues and Merge Requests') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>a</kbd>
+ </td>
+ <td>{{ __('Change assignee') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>m</kbd>
+ </td>
+ <td>{{ __('Change milestone') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Merge Requests') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>]</kbd>
+ /
+ <kbd>j</kbd>
+ </td>
+ <td>{{ __('Next file in diff') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>[</kbd>
+ /
+ <kbd>k</kbd>
+ </td>
+ <td>{{ __('Previous file in diff') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} p</kbd>
+ </td>
+ <td>{{ __('Go to file') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>n</kbd>
+ </td>
+ <td>{{ __('Next unresolved discussion') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>p</kbd>
+ </td>
+ <td>{{ __('Previous unresolved discussion') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>b</kbd>
+ </td>
+ <td>{{ __('Copy source branch name') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Merge Request Commits') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>c</kbd>
+ </td>
+ <td>{{ __('Next commit') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>x</kbd>
+ </td>
+ <td>{{ __('Previous commit') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Web IDE') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} p</kbd>
+ </td>
+ <td>{{ __('Go to file') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} enter</kbd>
+ </td>
+ <td>{{ __('Commit (when editing commit message)') }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 5e8ddeb6af7..476745beb19 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import Mousetrap from 'mousetrap';
-import Sidebar from '../../right_sidebar';
-import Shortcuts from './shortcuts';
-import { CopyAsGFM } from '../markdown/copy_as_gfm';
+import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { isElementVisible } from '~/lib/utils/dom_utils';
-import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard';
+import Sidebar from '../../right_sidebar';
+import { CopyAsGFM } from '../markdown/copy_as_gfm';
+import Shortcuts from './shortcuts';
export default class ShortcutsIssuable extends Shortcuts {
constructor() {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
index 8b7e6a56d25..c609936a02a 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
@@ -1,6 +1,6 @@
import Mousetrap from 'mousetrap';
-import ShortcutsNavigation from './shortcuts_navigation';
import findAndFollowLink from '../../lib/utils/navigation_utility';
+import ShortcutsNavigation from './shortcuts_navigation';
export default class ShortcutsWiki extends ShortcutsNavigation {
constructor() {
diff --git a/app/assets/javascripts/blob/3d_viewer/index.js b/app/assets/javascripts/blob/3d_viewer/index.js
index 94397d70384..fd064e7ca8f 100644
--- a/app/assets/javascripts/blob/3d_viewer/index.js
+++ b/app/assets/javascripts/blob/3d_viewer/index.js
@@ -1,6 +1,6 @@
-import * as THREE from 'three/build/three.module';
-import STLLoaderClass from 'three-stl-loader';
import OrbitControlsClass from 'three-orbit-controls';
+import STLLoaderClass from 'three-stl-loader';
+import * as THREE from 'three/build/three.module';
import MeshObject from './mesh_object';
const STLLoader = STLLoaderClass(THREE);
diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
index ebe2c2b3bb8..313bec7e01a 100644
--- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
@@ -1,5 +1,5 @@
-import sqljs from 'sql.js';
import { template as _template } from 'lodash';
+import sqljs from 'sql.js';
import axios from '~/lib/utils/axios_utils';
import { successCodes } from '~/lib/utils/http_status';
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index fc86f630c4e..c9152db509a 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -1,6 +1,6 @@
+import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '../flash';
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
-import { __ } from '~/locale';
function onError() {
const flash = new Flash(__('Balsamiq file could not be loaded.'));
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 19bad64155d..e72c5c90986 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,11 +1,11 @@
/* eslint-disable func-names */
-import $ from 'jquery';
import Dropzone from 'dropzone';
-import { visitUrl } from '../lib/utils/url_utility';
+import $ from 'jquery';
+import { sprintf, __ } from '~/locale';
import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf';
-import { sprintf, __ } from '~/locale';
+import { visitUrl } from '../lib/utils/url_utility';
Dropzone.autoDiscover = false;
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index a4a43b7a94e..9c023235428 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -1,7 +1,7 @@
<script>
-import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import DefaultActions from './blob_header_default_actions.vue';
import BlobFilepath from './blob_header_filepath.vue';
+import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import { SIMPLE_BLOB_VIEWER } from './constants';
export default {
@@ -71,7 +71,7 @@ export default {
</template>
</blob-filepath>
- <div class="gl-display-none gl-display-sm-flex">
+ <div class="gl-display-none gl-sm-display-flex">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" />
<slot name="actions"></slot>
diff --git a/app/assets/javascripts/blob/components/blob_header_filepath.vue b/app/assets/javascripts/blob/components/blob_header_filepath.vue
index eb8068a8ad7..99fe3938046 100644
--- a/app/assets/javascripts/blob/components/blob_header_filepath.vue
+++ b/app/assets/javascripts/blob/components/blob_header_filepath.vue
@@ -1,7 +1,7 @@
<script>
-import FileIcon from '~/vue_shared/components/file_icon.vue';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/blob/components/constants.js b/app/assets/javascripts/blob/components/constants.js
index 0137bd38d28..a129c537fa5 100644
--- a/app/assets/javascripts/blob/components/constants.js
+++ b/app/assets/javascripts/blob/components/constants.js
@@ -1,5 +1,5 @@
-import { __, sprintf } from '~/locale';
import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { __, sprintf } from '~/locale';
export const BTN_COPY_CONTENTS_TITLE = __('Copy file contents');
export const BTN_RAW_TITLE = __('Open raw');
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index c35f9934004..77910850908 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -1,19 +1,19 @@
import $ from 'jquery';
import Api from '~/api';
-import toast from '~/vue_shared/plugins/global_toast';
-import { __ } from '~/locale';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
+import { __ } from '~/locale';
+import toast from '~/vue_shared/plugins/global_toast';
import { deprecatedCreateFlash as Flash } from '../flash';
-import FileTemplateTypeSelector from './template_selectors/type_selector';
-import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import BlobCiSyntaxYamlSelector from './template_selectors/ci_syntax_yaml_selector';
+import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector';
import MetricsDashboardSelector from './template_selectors/metrics_dashboard_selector';
+import FileTemplateTypeSelector from './template_selectors/type_selector';
export default class FileTemplateMediator {
constructor({ editor, currentAction, projectId }) {
diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js
index 2532aeea989..a5c8050b772 100644
--- a/app/assets/javascripts/blob/file_template_selector.js
+++ b/app/assets/javascripts/blob/file_template_selector.js
@@ -66,6 +66,8 @@ export default class FileTemplateSelector {
reportSelectionName(options) {
const opts = options;
opts.query = options.selectedObj.name;
+ opts.data = options.selectedObj;
+ opts.data.source_template_project_id = options.selectedObj.project_id;
this.reportSelection(opts);
}
diff --git a/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js
index 9370e170571..c30ff4f1290 100644
--- a/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class BlobCiSyntaxYamlSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
index 3879a6c5742..0cdfd153675 100644
--- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class BlobCiYamlSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
index 5d976c5acdb..b48b3d6bec3 100644
--- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
@@ -1,6 +1,6 @@
-import FileTemplateSelector from '../file_template_selector';
-import { __ } from '~/locale';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __ } from '~/locale';
+import FileTemplateSelector from '../file_template_selector';
export default class DockerfileSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
index 1bb1cbb74de..50a11692e98 100644
--- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class BlobGitignoreSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js
index affa20997e9..e7fabf18ea1 100644
--- a/app/assets/javascripts/blob/template_selectors/license_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/license_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class BlobLicenseSelector extends FileTemplateSelector {
constructor({ mediator }) {
@@ -30,6 +30,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
const data = {
project: this.$dropdown.data('project'),
fullname: this.$dropdown.data('fullname'),
+ source_template_project_id: query.project_id,
};
this.reportSelection({
diff --git a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
index 42adab05ce3..8b10b02ae1d 100644
--- a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class MetricsDashboardSelector extends FileTemplateSelector {
constructor({ mediator }) {
diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js
index f74f7535d99..65e7ff0594c 100644
--- a/app/assets/javascripts/blob/template_selectors/type_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/type_selector.js
@@ -1,5 +1,5 @@
-import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import FileTemplateSelector from '../file_template_selector';
export default class FileTemplateTypeSelector extends FileTemplateSelector {
constructor({ mediator, dropdownData }) {
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 4e6ec20ec64..b4cd0d5d875 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
+import { __ } from '~/locale';
+import { fixTitle } from '~/tooltips';
import { deprecatedCreateFlash as Flash } from '../../flash';
-import { handleLocationHash } from '../../lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
+import { handleLocationHash } from '../../lib/utils/common_utils';
import eventHub from '../../notes/event_hub';
-import { __ } from '~/locale';
-import { fixTitle } from '~/tooltips';
const loadRichBlobViewer = (type) => {
switch (type) {
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 7c2217a59e9..173c82ef9b0 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -1,12 +1,12 @@
/* eslint-disable no-new */
import $ from 'jquery';
-import NewCommitForm from '../new_commit_form';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
+import BlobFileDropzone from '../blob/blob_file_dropzone';
+import NewCommitForm from '../new_commit_form';
const initPopovers = () => {
const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml');
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index c7f66a357f3..7c8f6646c0d 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,12 +1,12 @@
import $ from 'jquery';
-import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
-import TemplateSelectorMediator from '../blob/file_template_mediator';
-import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
import EditorLite from '~/editor/editor_lite';
import { FileTemplateExtension } from '~/editor/extensions/editor_file_template_ext';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
import { insertFinalNewline } from '~/lib/utils/text_utility';
+import TemplateSelectorMediator from '../blob/file_template_mediator';
+import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
export default class EditBlob {
// The options object has:
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 965d3571f42..13ad820477f 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,6 +1,6 @@
import { sortBy } from 'lodash';
-import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ListType, NOT_FILTER } from './constants';
export function getMilestone() {
return null;
@@ -144,6 +144,17 @@ export function isListDraggable(list) {
return list.listType !== ListType.backlog && list.listType !== ListType.closed;
}
+export function transformNotFilters(filters) {
+ return Object.keys(filters)
+ .filter((key) => key.startsWith(NOT_FILTER))
+ .reduce((obj, key) => {
+ return {
+ ...obj,
+ [key.substring(4, key.length - 1)]: filters[key],
+ };
+ }, {});
+}
+
// EE-specific feature. Find the implementation in the `ee/`-folder
export function transformBoardConfig() {
return '';
@@ -157,4 +168,5 @@ export default {
fullLabelId,
fullIterationId,
isListDraggable,
+ transformNotFilters,
};
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
new file mode 100644
index 00000000000..85fca589279
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
@@ -0,0 +1,21 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { mapActions } from 'vuex';
+
+export default {
+ components: {
+ GlButton,
+ },
+ methods: {
+ ...mapActions(['setAddColumnFormVisibility']),
+ },
+};
+</script>
+
+<template>
+ <span class="gl-ml-3 gl-display-flex gl-align-items-center">
+ <gl-button variant="confirm" @click="setAddColumnFormVisibility(true)"
+ >{{ __('Create list') }}
+ </gl-button>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
deleted file mode 100644
index 5d381f9a570..00000000000
--- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
+++ /dev/null
@@ -1,196 +0,0 @@
-<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
-import { cloneDeep } from 'lodash';
-import {
- GlDropdownItem,
- GlDropdownDivider,
- GlAvatarLabeled,
- GlAvatarLink,
- GlSearchBoxByType,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { __, n__ } from '~/locale';
-import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
-import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
-import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
-import searchUsers from '~/boards/graphql/users_search.query.graphql';
-
-export default {
- noSearchDelay: 0,
- searchDelay: 250,
- i18n: {
- unassigned: __('Unassigned'),
- assignee: __('Assignee'),
- assignees: __('Assignees'),
- assignTo: __('Assign to'),
- },
- components: {
- BoardEditableItem,
- IssuableAssignees,
- MultiSelectDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlAvatarLabeled,
- GlAvatarLink,
- GlSearchBoxByType,
- GlLoadingIcon,
- },
- data() {
- return {
- search: '',
- participants: [],
- selected: [],
- };
- },
- apollo: {
- participants: {
- query() {
- return this.isSearchEmpty ? getIssueParticipants : searchUsers;
- },
- variables() {
- if (this.isSearchEmpty) {
- return {
- id: `gid://gitlab/Issue/${this.activeIssue.iid}`,
- };
- }
-
- return {
- search: this.search,
- };
- },
- update(data) {
- if (this.isSearchEmpty) {
- return data.issue?.participants?.nodes || [];
- }
-
- return data.users?.nodes || [];
- },
- debounce() {
- const { noSearchDelay, searchDelay } = this.$options;
-
- return this.isSearchEmpty ? noSearchDelay : searchDelay;
- },
- },
- },
- computed: {
- ...mapGetters(['activeIssue']),
- ...mapState(['isSettingAssignees']),
- assigneeText() {
- return n__('Assignee', '%d Assignees', this.selected.length);
- },
- unSelectedFiltered() {
- return this.participants.filter(({ username }) => {
- return !this.selectedUserNames.includes(username);
- });
- },
- selectedIsEmpty() {
- return this.selected.length === 0;
- },
- selectedUserNames() {
- return this.selected.map(({ username }) => username);
- },
- isSearchEmpty() {
- return this.search === '';
- },
- currentUser() {
- return gon?.current_username;
- },
- },
- created() {
- this.selected = cloneDeep(this.activeIssue.assignees);
- },
- methods: {
- ...mapActions(['setAssignees']),
- async assignSelf() {
- const [currentUserObject] = await this.setAssignees(this.currentUser);
-
- this.selectAssignee(currentUserObject);
- },
- clearSelected() {
- this.selected = [];
- },
- selectAssignee(name) {
- if (name === undefined) {
- this.clearSelected();
- return;
- }
-
- this.selected = this.selected.concat(name);
- },
- unselect(name) {
- this.selected = this.selected.filter((user) => user.username !== name);
- },
- saveAssignees() {
- this.setAssignees(this.selectedUserNames);
- },
- isChecked(id) {
- return this.selectedUserNames.includes(id);
- },
- },
-};
-</script>
-
-<template>
- <board-editable-item :loading="isSettingAssignees" :title="assigneeText" @close="saveAssignees">
- <template #collapsed>
- <issuable-assignees :users="activeIssue.assignees" @assign-self="assignSelf" />
- </template>
-
- <template #default>
- <multi-select-dropdown
- class="w-100"
- :text="$options.i18n.assignees"
- :header-text="$options.i18n.assignTo"
- >
- <template #search>
- <gl-search-box-by-type v-model.trim="search" />
- </template>
- <template #items>
- <gl-loading-icon v-if="$apollo.queries.participants.loading" size="lg" />
- <template v-else>
- <gl-dropdown-item
- :is-checked="selectedIsEmpty"
- data-testid="unassign"
- class="mt-2"
- @click="selectAssignee()"
- >{{ $options.i18n.unassigned }}</gl-dropdown-item
- >
- <gl-dropdown-divider data-testid="unassign-divider" />
- <gl-dropdown-item
- v-for="item in selected"
- :key="item.id"
- :is-checked="isChecked(item.username)"
- @click="unselect(item.username)"
- >
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="item.name"
- :sub-label="item.username"
- :src="item.avatarUrl || item.avatar"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
- <gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" />
- <gl-dropdown-item
- v-for="unselectedUser in unSelectedFiltered"
- :key="unselectedUser.id"
- :data-testid="`item_${unselectedUser.name}`"
- @click="selectAssignee(unselectedUser)"
- >
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="unselectedUser.name"
- :sub-label="unselectedUser.username"
- :src="unselectedUser.avatarUrl || unselectedUser.avatar"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
- </template>
- </template>
- </multi-select-dropdown>
- </template>
- </board-editable-item>
-</template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 31050eef83d..e6009343626 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,13 +1,14 @@
<script>
-import BoardCardLayout from './board_card_layout.vue';
-import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
+import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
+import BoardCardLayout from './board_card_layout.vue';
+import BoardCardLayoutDeprecated from './board_card_layout_deprecated.vue';
export default {
name: 'BoardsIssueCard',
components: {
- BoardCardLayout,
+ BoardCardLayout: gon.features?.graphqlBoardLists ? BoardCardLayout : BoardCardLayoutDeprecated,
},
props: {
list: {
diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue
index 0a2301394c1..5e3c3702519 100644
--- a/app/assets/javascripts/boards/components/board_card_layout.vue
+++ b/app/assets/javascripts/boards/components/board_card_layout.vue
@@ -1,17 +1,13 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
-import IssueCardInner from './issue_card_inner.vue';
-import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
-import boardsStore from '../stores/boards_store';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { ISSUABLE } from '~/boards/constants';
+import IssueCardInner from './issue_card_inner.vue';
export default {
name: 'BoardCardLayout',
components: {
- IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
+ IssueCardInner,
},
- mixins: [glFeatureFlagMixin()],
props: {
list: {
type: Object,
@@ -42,17 +38,17 @@ export default {
data() {
return {
showDetail: false,
- multiSelect: boardsStore.multiSelect,
};
},
computed: {
+ ...mapState(['selectedBoardItems']),
...mapGetters(['isSwimlanesOn']),
multiSelectVisible() {
- return this.multiSelect.list.findIndex((issue) => issue.id === this.issue.id) > -1;
+ return this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.issue.id) > -1;
},
},
methods: {
- ...mapActions(['setActiveId']),
+ ...mapActions(['setActiveId', 'toggleBoardItemMultiSelection']),
mouseDown() {
this.showDetail = true;
},
@@ -63,16 +59,16 @@ export default {
// Don't do anything if this happened on a no trigger element
if (e.target.classList.contains('js-no-trigger')) return;
- if (this.glFeatures.graphqlBoardLists || this.isSwimlanesOn) {
+ const isMultiSelect = e.ctrlKey || e.metaKey;
+
+ if (!isMultiSelect) {
this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
- return;
+ } else {
+ this.toggleBoardItemMultiSelection(this.issue);
}
- const isMultiSelect = e.ctrlKey || e.metaKey;
-
if (this.showDetail || isMultiSelect) {
this.showDetail = false;
- this.$emit('show', { event: e, isMultiSelect });
}
},
},
diff --git a/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue b/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
new file mode 100644
index 00000000000..f9a726134a3
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
@@ -0,0 +1,102 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import { ISSUABLE } from '~/boards/constants';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import boardsStore from '../stores/boards_store';
+import IssueCardInner from './issue_card_inner.vue';
+import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
+
+export default {
+ name: 'BoardCardLayout',
+ components: {
+ IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ issue: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ required: false,
+ },
+ isActive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ showDetail: false,
+ multiSelect: boardsStore.multiSelect,
+ };
+ },
+ computed: {
+ ...mapGetters(['isSwimlanesOn']),
+ multiSelectVisible() {
+ return this.multiSelect.list.findIndex((issue) => issue.id === this.issue.id) > -1;
+ },
+ },
+ methods: {
+ ...mapActions(['setActiveId']),
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ // Don't do anything if this happened on a no trigger element
+ if (e.target.classList.contains('js-no-trigger')) return;
+
+ if (this.glFeatures.graphqlBoardLists || this.isSwimlanesOn) {
+ this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
+ return;
+ }
+
+ const isMultiSelect = e.ctrlKey || e.metaKey;
+
+ if (this.showDetail || isMultiSelect) {
+ this.showDetail = false;
+ this.$emit('show', { event: e, isMultiSelect });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ :class="{
+ 'multi-select': multiSelectVisible,
+ 'user-can-drag': !disabled && issue.id,
+ 'is-disabled': disabled || !issue.id,
+ 'is-active': isActive,
+ }"
+ :index="index"
+ :data-issue-id="issue.id"
+ :data-issue-iid="issue.iid"
+ :data-issue-path="issue.referencePath"
+ data-testid="board_card"
+ class="board-card gl-p-5 gl-rounded-base"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)"
+ >
+ <issue-card-inner :list="list" :issue="issue" :update-filters="true" />
+ </li>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 9f0eef844f6..41b9ee795eb 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters, mapActions, mapState } from 'vuex';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
-import BoardList from './board_list.vue';
import { isListDraggable } from '../boards_util';
+import BoardList from './board_list.vue';
export default {
components: {
@@ -31,8 +31,11 @@ export default {
},
},
computed: {
- ...mapState(['filterParams']),
+ ...mapState(['filterParams', 'highlightedLists']),
...mapGetters(['getIssuesByList']),
+ highlighted() {
+ return this.highlightedLists.includes(this.list.id);
+ },
listIssues() {
return this.getIssuesByList(this.list.id);
},
@@ -48,6 +51,16 @@ export default {
deep: true,
immediate: true,
},
+ highlighted: {
+ handler(highlighted) {
+ if (highlighted) {
+ this.$nextTick(() => {
+ this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ }
+ },
+ immediate: true,
+ },
},
methods: {
...mapActions(['fetchIssuesForList']),
@@ -68,6 +81,7 @@ export default {
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
+ :class="{ 'board-column-highlighted': highlighted }"
>
<board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list
diff --git a/app/assets/javascripts/boards/components/board_column_deprecated.vue b/app/assets/javascripts/boards/components/board_column_deprecated.vue
index 35688efceb4..3dc77654e28 100644
--- a/app/assets/javascripts/boards/components/board_column_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_column_deprecated.vue
@@ -2,9 +2,9 @@
// This component is being replaced in favor of './board_column.vue' for GraphQL boards
import Sortable from 'sortablejs';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_deprecated.vue';
-import BoardList from './board_list_deprecated.vue';
-import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
+import boardsStore from '../stores/boards_store';
+import BoardList from './board_list_deprecated.vue';
export default {
components: {
@@ -46,6 +46,7 @@ export default {
watch: {
filter: {
handler() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.page = 1;
this.list.getIssues(true).catch(() => {
// TODO: handle request error
@@ -53,6 +54,16 @@ export default {
},
deep: true,
},
+ 'list.highlighted': {
+ handler(highlighted) {
+ if (highlighted) {
+ this.$nextTick(() => {
+ this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ }
+ },
+ immediate: true,
+ },
},
mounted() {
const instance = this;
@@ -97,6 +108,7 @@ export default {
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
+ :class="{ 'board-column-highlighted': list.highlighted }"
>
<board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" />
diff --git a/app/assets/javascripts/boards/components/board_configuration_options.vue b/app/assets/javascripts/boards/components/board_configuration_options.vue
index b8ee930a8c9..4d79f2a4bc6 100644
--- a/app/assets/javascripts/boards/components/board_configuration_options.vue
+++ b/app/assets/javascripts/boards/components/board_configuration_options.vue
@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: true,
},
+ readonly: {
+ type: Boolean,
+ required: true,
+ },
},
};
</script>
@@ -28,12 +32,14 @@ export default {
</p>
<gl-form-checkbox
:checked="!hideBacklogList"
+ :disabled="readonly"
data-testid="backlog-list-checkbox"
@change="$emit('update:hideBacklogList', !hideBacklogList)"
>{{ __('Show the Open list') }}
</gl-form-checkbox>
<gl-form-checkbox
:checked="!hideClosedList"
+ :disabled="readonly"
data-testid="closed-list-checkbox"
@change="$emit('update:hideClosedList', !hideClosedList)"
>{{ __('Show the Closed list') }}
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 19254343208..9b10e7d7db5 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,13 +1,13 @@
<script>
+import { GlAlert } from '@gitlab/ui';
+import { sortBy } from 'lodash';
import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
-import { sortBy } from 'lodash';
-import { GlAlert } from '@gitlab/ui';
-import BoardColumnDeprecated from './board_column_deprecated.vue';
-import BoardColumn from './board_column.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import defaultSortableConfig from '~/sortable/sortable_config';
import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options';
+import defaultSortableConfig from '~/sortable/sortable_config';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import BoardColumn from './board_column.vue';
+import BoardColumnDeprecated from './board_column_deprecated.vue';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index c701ecd3040..f65f00bcccc 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,17 +1,17 @@
<script>
import { GlModal } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
-import { visitUrl } from '~/lib/utils/url_utility';
-import { getParameterByName } from '~/lib/utils/common_utils';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import boardsStore from '~/boards/stores/boards_store';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { __, s__ } from '~/locale';
import { fullLabelId, fullBoardId } from '../boards_util';
+import { formType } from '../constants';
-import BoardConfigurationOptions from './board_configuration_options.vue';
-import updateBoardMutation from '../graphql/board_update.mutation.graphql';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
import destroyBoardMutation from '../graphql/board_destroy.mutation.graphql';
+import updateBoardMutation from '../graphql/board_update.mutation.graphql';
+import BoardConfigurationOptions from './board_configuration_options.vue';
const boardDefaults = {
id: false,
@@ -26,12 +26,6 @@ const boardDefaults = {
hide_closed_list: false,
};
-const formType = {
- new: 'new',
- delete: 'delete',
- edit: 'edit',
-};
-
export default {
i18n: {
[formType.new]: { title: s__('Board|Create new board'), btnText: s__('Board|Create board') },
@@ -100,11 +94,14 @@ export default {
type: Object,
required: true,
},
+ currentPage: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
board: { ...boardDefaults, ...this.currentBoard },
- currentPage: boardsStore.state.currentPage,
isLoading: false,
};
},
@@ -256,7 +253,7 @@ export default {
}
},
cancel() {
- boardsStore.showPage('');
+ this.$emit('cancel');
},
resetFormState() {
if (this.isNewForm) {
@@ -308,6 +305,7 @@ export default {
<board-configuration-options
:hide-backlog-list.sync="board.hide_backlog_list"
:hide-closed-list.sync="board.hide_closed_list"
+ :readonly="readonly"
/>
<board-scope
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index b6e4d0980fa..7495b1163be 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,13 +1,13 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import { mapActions, mapState } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
-import defaultSortableConfig from '~/sortable/sortable_config';
import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options';
-import BoardNewIssue from './board_new_issue.vue';
-import BoardCard from './board_card.vue';
-import eventHub from '../eventhub';
import { sprintf, __ } from '~/locale';
+import defaultSortableConfig from '~/sortable/sortable_config';
+import eventHub from '../eventhub';
+import BoardCard from './board_card.vue';
+import BoardNewIssue from './board_new_issue.vue';
export default {
name: 'BoardList',
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
index 24900346bda..9b4961d362d 100644
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue
@@ -1,17 +1,18 @@
<script>
-import { Sortable, MultiDrag } from 'sortablejs';
import { GlLoadingIcon } from '@gitlab/ui';
-import boardNewIssue from './board_new_issue_deprecated.vue';
-import boardCard from './board_card.vue';
-import eventHub from '../eventhub';
-import boardsStore from '../stores/boards_store';
-import { sprintf, __ } from '~/locale';
+import { Sortable, MultiDrag } from 'sortablejs';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import { sprintf, __ } from '~/locale';
+import eventHub from '../eventhub';
import {
getBoardSortableDefaultOptions,
sortableStart,
sortableEnd,
} from '../mixins/sortable_default_options';
+import boardsStore from '../stores/boards_store';
+import boardCard from './board_card.vue';
+import boardNewIssue from './board_new_issue_deprecated.vue';
// This component is being replaced in favor of './board_list.vue' for GraphQL boards
@@ -63,6 +64,7 @@ export default {
watch: {
filters: {
handler() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
@@ -75,6 +77,7 @@ export default {
this.list.issuesSize > this.list.issues.length &&
this.list.isExpanded
) {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.page += 1;
this.list.getIssues(false).catch(() => {
// TODO: handle request error
@@ -165,7 +168,7 @@ export default {
boardsStore.startMoving(list, issue);
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
sortableStart();
},
@@ -283,6 +286,7 @@ export default {
* issue indexes are far apart, this logic should ever kick in.
*/
setTimeout(() => {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.issues.splice(i, 1);
}, 0);
});
@@ -386,10 +390,12 @@ export default {
loadNextPage() {
const getIssues = this.list.nextPage();
const loadingDone = () => {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.loadingMore = false;
};
if (getIssues) {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.loadingMore = true;
getIssues.then(loadingDone).catch(loadingDone);
}
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 06f39eceb08..a933370427c 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapState } from 'vuex';
import {
GlButton,
GlButtonGroup,
@@ -9,14 +8,16 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { isListDraggable } from '~/boards/boards_util';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__, __ } from '~/locale';
-import AccessorUtilities from '../../lib/utils/accessor';
-import IssueCount from './issue_count.vue';
-import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
+import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType } from '../constants';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import { isListDraggable } from '~/boards/boards_util';
+import eventHub from '../eventhub';
+import IssueCount from './issue_count.vue';
export default {
i18n: {
@@ -85,16 +86,16 @@ export default {
return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
- return (
- this.listType === ListType.milestone &&
- this.list.milestone &&
- (!this.list.collapsed || !this.isSwimlanesHeader)
- );
+ return this.listType === ListType.milestone && this.list.milestone && this.showListDetails;
},
showAssigneeListDetails() {
- return (
- this.listType === ListType.assignee && (!this.list.collapsed || !this.isSwimlanesHeader)
- );
+ return this.listType === ListType.assignee && this.showListDetails;
+ },
+ showIterationListDetails() {
+ return this.listType === ListType.iteration && this.showListDetails;
+ },
+ showListDetails() {
+ return !this.list.collapsed || !this.isSwimlanesHeader;
},
issuesCount() {
return this.list.issuesCount;
@@ -147,6 +148,7 @@ export default {
eventHub.$emit(`toggle-issue-form-${this.list.id}`);
},
toggleExpanded() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.collapsed = !this.list.collapsed;
if (!this.isLoggedIn) {
@@ -157,7 +159,7 @@ export default {
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
@@ -216,6 +218,17 @@ export default {
<gl-icon name="timer" />
</span>
+ <span
+ v-if="showIterationListDetails"
+ aria-hidden="true"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': list.collapsed,
+ 'gl-mr-2': !list.collapsed,
+ }"
+ >
+ <gl-icon name="iteration" />
+ </span>
+
<a
v-if="showAssigneeListDetails"
:href="list.assignee.webUrl"
diff --git a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
index 21147f1616c..ff043d3aa01 100644
--- a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapState } from 'vuex';
import {
GlButton,
GlButtonGroup,
@@ -9,14 +8,16 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__ } from '~/locale';
-import AccessorUtilities from '../../lib/utils/accessor';
-import IssueCount from './issue_count.vue';
-import boardsStore from '../stores/boards_store';
-import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
+import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType } from '../constants';
-import { isScopedLabel } from '~/lib/utils/common_utils';
+import eventHub from '../eventhub';
+import boardsStore from '../stores/boards_store';
+import IssueCount from './issue_count.vue';
// This component is being replaced in favor of './board_list_header.vue' for GraphQL boards
@@ -77,14 +78,16 @@ export default {
return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
- return (
- this.list.type === 'milestone' &&
- this.list.milestone &&
- (this.list.isExpanded || !this.isSwimlanesHeader)
- );
+ return this.list.type === 'milestone' && this.list.milestone && this.showListDetails;
},
showAssigneeListDetails() {
- return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader);
+ return this.list.type === 'assignee' && this.showListDetails;
+ },
+ showIterationListDetails() {
+ return this.listType === ListType.iteration && this.showListDetails;
+ },
+ showListDetails() {
+ return this.list.isExpanded || !this.isSwimlanesHeader;
},
issuesCount() {
return this.list.issuesSize;
@@ -131,6 +134,7 @@ export default {
eventHub.$emit(`toggle-issue-form-${this.list.id}`);
},
toggleExpanded() {
+ // eslint-disable-next-line vue/no-mutating-props
this.list.isExpanded = !this.list.isExpanded;
if (!this.isLoggedIn) {
@@ -141,7 +145,7 @@ export default {
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
@@ -201,6 +205,17 @@ export default {
<gl-icon name="timer" />
</span>
+ <span
+ v-if="showIterationListDetails"
+ aria-hidden="true"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': !list.isExpanded,
+ 'gl-mr-2': list.isExpanded,
+ }"
+ >
+ <gl-icon name="iteration" />
+ </span>
+
<a
v-if="showAssigneeListDetails"
:href="list.assignee.path"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 14d28643046..1df154688c8 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,11 +1,11 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
+import { __ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { __ } from '~/locale';
export default {
name: 'BoardNewIssue',
diff --git a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
index 4fc58742783..eff87ff110e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
@@ -2,10 +2,10 @@
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import ListIssue from 'ee_else_ce/boards/models/issue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../eventhub';
-import ProjectSelect from './project_select_deprecated.vue';
import boardsStore from '../stores/boards_store';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ProjectSelect from './project_select_deprecated.vue';
// This component is being replaced in favor of './board_new_issue.vue' for GraphQL boards
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index f362fc60bd3..7cfedad0aed 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -1,21 +1,17 @@
<script>
import { GlButton, GlDrawer, GlLabel } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
-import { __ } from '~/locale';
+import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
-import eventHub from '~/sidebar/event_hub';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import { LIST } from '~/boards/constants';
+import { __ } from '~/locale';
+import eventHub from '~/sidebar/event_hub';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default {
headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px',
listSettingsText: __('List settings'),
- assignee: 'assignee',
- milestone: 'milestone',
- label: 'label',
- labelListText: __('Label'),
components: {
GlButton,
GlDrawer,
@@ -33,6 +29,11 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ ListType,
+ };
+ },
computed: {
...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
@@ -56,7 +57,7 @@ export default {
return this.activeList.type || this.activeList.listType || null;
},
listTypeTitle() {
- return this.$options.labelListText;
+ return ListTypeTitles[ListType.label];
},
showSidebar() {
return this.sidebarType === LIST;
@@ -98,7 +99,7 @@ export default {
>
<template #header>{{ $options.listSettingsText }}</template>
<template v-if="isSidebarOpen">
- <div v-if="boardListType === $options.label">
+ <div v-if="boardListType === ListType.label">
<label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
<gl-label
:title="activeListLabel.title"
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index bf3dc5c608f..6d5a13be3ac 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,23 +1,26 @@
-/* eslint-disable no-new */
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it
+// relies on app/views/shared/boards/components/_sidebar.html.haml for its
+// template.
+/* eslint-disable no-new, @gitlab/no-runtime-template-compiler */
+import { GlLabel } from '@gitlab/ui';
import $ from 'jquery';
import Vue from 'vue';
-import { GlLabel } from '@gitlab/ui';
-import { deprecatedCreateFlash as Flash } from '~/flash';
-import { sprintf, __ } from '~/locale';
-import Sidebar from '~/right_sidebar';
-import eventHub from '~/sidebar/event_hub';
import DueDateSelectors from '~/due_date_select';
import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { sprintf, __ } from '~/locale';
+import MilestoneSelect from '~/milestone_select';
+import Sidebar from '~/right_sidebar';
import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
import Assignees from '~/sidebar/components/assignees/assignees.vue';
+import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
-import MilestoneSelect from '~/milestone_select';
-import RemoveBtn from './sidebar/remove_issue.vue';
+import eventHub from '~/sidebar/event_hub';
import boardsStore from '../stores/boards_store';
-import { isScopedLabel } from '~/lib/utils/common_utils';
+import RemoveBtn from './sidebar/remove_issue.vue';
export default Vue.extend({
components: {
@@ -29,6 +32,7 @@ export default Vue.extend({
RemoveBtn,
Subscriptions,
TimeTracker,
+ SidebarAssigneesWidget,
},
props: {
currentUser: {
@@ -75,12 +79,6 @@ export default Vue.extend({
detail: {
handler() {
if (this.issue.id !== this.detail.issue.id) {
- $('.block.assignee')
- .find('input:not(.js-vue)[name="issue[assignee_ids][]"]')
- .each((i, el) => {
- $(el).remove();
- });
-
$('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el).data('deprecatedJQueryDropdown').clearMenu();
});
@@ -93,18 +91,9 @@ export default Vue.extend({
},
},
created() {
- // Get events from deprecatedJQueryDropdown
- eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
- eventHub.$on('sidebar.addAssignee', this.addAssignee);
- eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
- eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
eventHub.$on('sidebar.closeAll', this.closeSidebar);
},
beforeDestroy() {
- eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
- eventHub.$off('sidebar.addAssignee', this.addAssignee);
- eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
- eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
eventHub.$off('sidebar.closeAll', this.closeSidebar);
},
mounted() {
@@ -118,34 +107,8 @@ export default Vue.extend({
closeSidebar() {
this.detail.issue = {};
},
- 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) {
- boardsStore.detail.issue.removeAssignee(a);
- },
- addAssignee(a) {
- boardsStore.detail.issue.addAssignee(a);
- },
- removeAllAssignees() {
- boardsStore.detail.issue.removeAllAssignees();
- },
- saveAssignees() {
- this.loadingAssignees = true;
-
- boardsStore.detail.issue
- .update()
- .then(() => {
- this.loadingAssignees = false;
- })
- .catch(() => {
- this.loadingAssignees = false;
- Flash(__('An error occurred while saving assignees'));
- });
+ setAssignees(data) {
+ boardsStore.detail.issue.setAssignees(data.issueSetAssignees.issue.assignees.nodes);
},
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index fcd1c3fdceb..2a064aaa885 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -1,5 +1,4 @@
<script>
-import { throttle } from 'lodash';
import {
GlLoadingIcon,
GlSearchBoxByType,
@@ -9,14 +8,16 @@ import {
GlDropdownItem,
GlModalDirective,
} from '@gitlab/ui';
+import { throttle } from 'lodash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import projectQuery from '../graphql/project_boards.query.graphql';
+import eventHub from '../eventhub';
import groupQuery from '../graphql/group_boards.query.graphql';
+import projectQuery from '../graphql/project_boards.query.graphql';
-import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
@@ -35,6 +36,7 @@ export default {
directives: {
GlModalDirective,
},
+ inject: ['fullPath', 'recentBoardsEndpoint'],
props: {
currentBoard: {
type: Object,
@@ -99,12 +101,11 @@ export default {
scrollFadeInitialized: false,
boards: [],
recentBoards: [],
- state: boardsStore.state,
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
- store: boardsStore,
filterTerm: '',
+ currentPage: '',
};
},
computed: {
@@ -114,16 +115,13 @@ export default {
loading() {
return this.loadingRecentBoards || Boolean(this.loadingBoards);
},
- currentPage() {
- return this.state.currentPage;
- },
filteredBoards() {
return this.boards.filter((board) =>
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
board() {
- return this.state.currentBoard;
+ return this.currentBoard;
},
showDelete() {
return this.boards.length > 1;
@@ -148,11 +146,17 @@ export default {
},
},
created() {
- boardsStore.setCurrentBoard(this.currentBoard);
+ eventHub.$on('showBoardModal', this.showPage);
+ },
+ beforeDestroy() {
+ eventHub.$off('showBoardModal', this.showPage);
},
methods: {
showPage(page) {
- boardsStore.showPage(page);
+ this.currentPage = page;
+ },
+ cancel() {
+ this.showPage('');
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
@@ -161,7 +165,7 @@ export default {
this.$apollo.addSmartQuery('boards', {
variables() {
- return { fullPath: this.state.endpoints.fullPath };
+ return { fullPath: this.fullPath };
},
query() {
return this.groupId ? groupQuery : projectQuery;
@@ -179,8 +183,10 @@ export default {
});
this.loadingRecentBoards = true;
- boardsStore
- .recentBoards()
+ // Follow up to fetch recent boards using GraphQL
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/300985
+ axios
+ .get(this.recentBoardsEndpoint)
.then((res) => {
this.recentBoards = res.data;
})
@@ -346,6 +352,8 @@ export default {
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:current-board="currentBoard"
+ :current-page="currentPage"
+ @cancel="cancel"
/>
</span>
</div>
diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
new file mode 100644
index 00000000000..33ad46a0d29
--- /dev/null
+++ b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
@@ -0,0 +1,357 @@
+<script>
+import {
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlModalDirective,
+} from '@gitlab/ui';
+import { throttle } from 'lodash';
+
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import httpStatusCodes from '~/lib/utils/http_status';
+
+import groupQuery from '../graphql/group_boards.query.graphql';
+import projectQuery from '../graphql/project_boards.query.graphql';
+
+import boardsStore from '../stores/boards_store';
+import BoardForm from './board_form.vue';
+
+const MIN_BOARDS_TO_VIEW_RECENT = 10;
+
+export default {
+ name: 'BoardsSelector',
+ components: {
+ BoardForm,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ props: {
+ currentBoard: {
+ type: Object,
+ required: true,
+ },
+ throttleDuration: {
+ type: Number,
+ default: 200,
+ required: false,
+ },
+ boardBaseUrl: {
+ type: String,
+ required: true,
+ },
+ hasMissingBoards: {
+ type: Boolean,
+ required: true,
+ },
+ canAdminBoard: {
+ type: Boolean,
+ required: true,
+ },
+ multipleIssueBoardsAvailable: {
+ type: Boolean,
+ required: true,
+ },
+ labelsPath: {
+ type: String,
+ required: true,
+ },
+ labelsWebUrl: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ groupId: {
+ type: Number,
+ required: true,
+ },
+ scopedIssueBoardFeatureEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ weights: {
+ type: Array,
+ required: true,
+ },
+ enabledScopedLabels: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ hasScrollFade: false,
+ loadingBoards: 0,
+ loadingRecentBoards: false,
+ scrollFadeInitialized: false,
+ boards: [],
+ recentBoards: [],
+ state: boardsStore.state,
+ throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
+ contentClientHeight: 0,
+ maxPosition: 0,
+ store: boardsStore,
+ filterTerm: '',
+ };
+ },
+ computed: {
+ parentType() {
+ return this.groupId ? 'group' : 'project';
+ },
+ loading() {
+ return this.loadingRecentBoards || Boolean(this.loadingBoards);
+ },
+ currentPage() {
+ return this.state.currentPage;
+ },
+ filteredBoards() {
+ return this.boards.filter((board) =>
+ board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
+ );
+ },
+ board() {
+ return this.state.currentBoard;
+ },
+ showDelete() {
+ return this.boards.length > 1;
+ },
+ scrollFadeClass() {
+ return {
+ 'fade-out': !this.hasScrollFade,
+ };
+ },
+ showRecentSection() {
+ return (
+ this.recentBoards.length &&
+ this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
+ !this.filterTerm.length
+ );
+ },
+ },
+ watch: {
+ filteredBoards() {
+ this.scrollFadeInitialized = false;
+ this.$nextTick(this.setScrollFade);
+ },
+ },
+ created() {
+ boardsStore.setCurrentBoard(this.currentBoard);
+ },
+ methods: {
+ showPage(page) {
+ boardsStore.showPage(page);
+ },
+ cancel() {
+ this.showPage('');
+ },
+ loadBoards(toggleDropdown = true) {
+ if (toggleDropdown && this.boards.length > 0) {
+ return;
+ }
+
+ this.$apollo.addSmartQuery('boards', {
+ variables() {
+ return { fullPath: this.state.endpoints.fullPath };
+ },
+ query() {
+ return this.groupId ? groupQuery : projectQuery;
+ },
+ loadingKey: 'loadingBoards',
+ update(data) {
+ if (!data?.[this.parentType]) {
+ return [];
+ }
+ return data[this.parentType].boards.edges.map(({ node }) => ({
+ id: getIdFromGraphQLId(node.id),
+ name: node.name,
+ }));
+ },
+ });
+
+ this.loadingRecentBoards = true;
+ boardsStore
+ .recentBoards()
+ .then((res) => {
+ this.recentBoards = res.data;
+ })
+ .catch((err) => {
+ /**
+ * If user is unauthorized we'd still want to resolve the
+ * request to display all boards.
+ */
+ if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
+ this.recentBoards = []; // recent boards are empty
+ return;
+ }
+ throw err;
+ })
+ .then(() => this.$nextTick()) // Wait for boards list in DOM
+ .then(() => {
+ this.setScrollFade();
+ })
+ .catch(() => {})
+ .finally(() => {
+ this.loadingRecentBoards = false;
+ });
+ },
+ isScrolledUp() {
+ const { content } = this.$refs;
+
+ if (!content) {
+ return false;
+ }
+
+ const currentPosition = this.contentClientHeight + content.scrollTop;
+
+ return currentPosition < this.maxPosition;
+ },
+ initScrollFade() {
+ const { content } = this.$refs;
+
+ if (!content) {
+ return;
+ }
+
+ this.scrollFadeInitialized = true;
+
+ this.contentClientHeight = content.clientHeight;
+ this.maxPosition = content.scrollHeight;
+ },
+ setScrollFade() {
+ if (!this.scrollFadeInitialized) this.initScrollFade();
+
+ this.hasScrollFade = this.isScrolledUp();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="boards-switcher js-boards-selector gl-mr-3">
+ <span class="boards-selector-wrapper js-boards-selector-wrapper">
+ <gl-dropdown
+ data-qa-selector="boards_dropdown"
+ toggle-class="dropdown-menu-toggle js-dropdown-toggle"
+ menu-class="flex-column dropdown-extended-height"
+ :text="board.name"
+ @show="loadBoards"
+ >
+ <p class="gl-new-dropdown-header-top" @mousedown.prevent>
+ {{ s__('IssueBoards|Switch board') }}
+ </p>
+ <gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
+
+ <div
+ v-if="!loading"
+ ref="content"
+ data-qa-selector="boards_dropdown_content"
+ class="dropdown-content flex-fill"
+ @scroll.passive="throttledSetScrollFade"
+ >
+ <gl-dropdown-item
+ v-show="filteredBoards.length === 0"
+ class="gl-pointer-events-none text-secondary"
+ >
+ {{ s__('IssueBoards|No matching boards found') }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-section-header v-if="showRecentSection">
+ {{ __('Recent') }}
+ </gl-dropdown-section-header>
+
+ <template v-if="showRecentSection">
+ <gl-dropdown-item
+ v-for="recentBoard in recentBoards"
+ :key="`recent-${recentBoard.id}`"
+ class="js-dropdown-item"
+ :href="`${boardBaseUrl}/${recentBoard.id}`"
+ >
+ {{ recentBoard.name }}
+ </gl-dropdown-item>
+ </template>
+
+ <gl-dropdown-divider v-if="showRecentSection" />
+
+ <gl-dropdown-section-header v-if="showRecentSection">
+ {{ __('All') }}
+ </gl-dropdown-section-header>
+
+ <gl-dropdown-item
+ v-for="otherBoard in filteredBoards"
+ :key="otherBoard.id"
+ class="js-dropdown-item"
+ :href="`${boardBaseUrl}/${otherBoard.id}`"
+ >
+ {{ otherBoard.name }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events">
+ {{
+ s__(
+ 'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
+ )
+ }}
+ </gl-dropdown-item>
+ </div>
+
+ <div
+ v-show="filteredBoards.length > 0"
+ class="dropdown-content-faded-mask"
+ :class="scrollFadeClass"
+ ></div>
+
+ <gl-loading-icon v-if="loading" />
+
+ <div v-if="canAdminBoard">
+ <gl-dropdown-divider />
+
+ <gl-dropdown-item
+ v-if="multipleIssueBoardsAvailable"
+ v-gl-modal-directive="'board-config-modal'"
+ data-qa-selector="create_new_board_button"
+ @click.prevent="showPage('new')"
+ >
+ {{ s__('IssueBoards|Create new board') }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-item
+ v-if="showDelete"
+ v-gl-modal-directive="'board-config-modal'"
+ class="text-danger js-delete-board"
+ @click.prevent="showPage('delete')"
+ >
+ {{ s__('IssueBoards|Delete board') }}
+ </gl-dropdown-item>
+ </div>
+ </gl-dropdown>
+
+ <board-form
+ v-if="currentPage"
+ :labels-path="labelsPath"
+ :labels-web-url="labelsWebUrl"
+ :project-id="projectId"
+ :group-id="groupId"
+ :can-admin-board="canAdminBoard"
+ :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
+ :weights="weights"
+ :enable-scoped-labels="enabledScopedLabels"
+ :current-board="currentBoard"
+ :current-page="state.currentPage"
+ @cancel="cancel"
+ />
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 457d0d4dcd6..e5ea30df767 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,17 +1,17 @@
<script>
+import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapActions, mapState } from 'vuex';
-import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { updateHistory } from '~/lib/utils/url_utility';
import { sprintf, __, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import { ListType } from '../constants';
+import eventHub from '../eventhub';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
-import eventHub from '../eventhub';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import { ListType } from '../constants';
-import { updateHistory } from '~/lib/utils/url_utility';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
index 75cf1f0b9e1..069cc2cda22 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
@@ -1,15 +1,15 @@
<script>
+import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapState } from 'vuex';
-import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
+import { isScopedLabel } from '~/lib/utils/common_utils';
import { sprintf, __, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import boardsStore from '../stores/boards_store';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate_deprecated.vue';
-import boardsStore from '../stores/boards_store';
-import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index fb45de6e14d..7e3f36c8a17 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -1,13 +1,13 @@
<script>
-import dateFormat from 'dateformat';
import { GlTooltip, GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
+import dateFormat from 'dateformat';
import {
getDayDifference,
getTimeago,
dateInWords,
parsePikadayDate,
} from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
index f6b00b695da..42d187b9b40 100644
--- a/app/assets/javascripts/boards/components/issue_time_estimate.vue
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltip, GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
export default {
i18n: {
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index eb2db260717..486b012e3d2 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
-import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
+import ModalStore from '../../stores/modal_store';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js
index 56a0fde5a91..2fb38a549f3 100644
--- a/app/assets/javascripts/boards/components/modal/filters.js
+++ b/app/assets/javascripts/boards/components/modal/filters.js
@@ -1,5 +1,5 @@
-import FilteredSearchBoards from '../../filtered_search_boards';
import FilteredSearchContainer from '../../../filtered_search/container';
+import FilteredSearchBoards from '../../filtered_search_boards';
export default {
name: 'modal-filters',
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index 10c29977cae..05e1219bc70 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -3,10 +3,10 @@ import { GlButton } from '@gitlab/ui';
import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __, n__ } from '../../../locale';
-import ListsDropdown from './lists_dropdown.vue';
-import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
import boardsStore from '../../stores/boards_store';
+import ModalStore from '../../stores/modal_store';
+import ListsDropdown from './lists_dropdown.vue';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 3e96ecca24c..c3a71e7177a 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -2,10 +2,10 @@
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
+import modalMixin from '../../mixins/modal_mixins';
+import ModalStore from '../../stores/modal_store';
import ModalFilters from './filters';
import ModalTabs from './tabs.vue';
-import ModalStore from '../../stores/modal_store';
-import modalMixin from '../../mixins/modal_mixins';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 84d687a46b9..5af90c1ee66 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -1,13 +1,13 @@
<script>
/* global ListIssue */
import { GlLoadingIcon } from '@gitlab/ui';
-import { urlParamsToObject } from '~/lib/utils/common_utils';
import boardsStore from '~/boards/stores/boards_store';
+import { urlParamsToObject } from '~/lib/utils/common_utils';
+import ModalStore from '../../stores/modal_store';
+import EmptyState from './empty_state.vue';
+import ModalFooter from './footer.vue';
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';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index 219263bd9b9..bf69f8140d5 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -1,6 +1,6 @@
<script>
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { GlIcon } from '@gitlab/ui';
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import ModalStore from '../../stores/modal_store';
import IssueCardInner from '../issue_card_inner.vue';
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
index fe10e7fb856..2065568d275 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
-import ModalStore from '../../stores/modal_store';
import boardsStore from '../../stores/boards_store';
+import ModalStore from '../../stores/modal_store';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
index b066fb25360..0b717f516db 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.vue
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
-import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
+import ModalStore from '../../stores/modal_store';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 2bc54155163..2fd16f06455 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -1,15 +1,15 @@
/* eslint-disable func-names, no-new */
import $ from 'jquery';
-import { __ } from '~/locale';
-import axios from '~/lib/utils/axios_utils';
+import store from '~/boards/stores';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
import CreateLabelDropdown from '../../create_label';
-import boardsStore from '../stores/boards_store';
import { fullLabelId } from '../boards_util';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import store from '~/boards/stores';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import boardsStore from '../stores/boards_store';
function shouldCreateListGraphQL(label) {
return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label));
@@ -51,16 +51,27 @@ export default function initNewListDropdown() {
initDeprecatedJQueryDropdown($dropdownToggle, {
data(term, callback) {
- axios
- .get($dropdownToggle.attr('data-list-labels-path'))
- .then(({ data }) => callback(data))
- .catch(() => {
- $dropdownToggle.data('bs.dropdown').hide();
- flash(__('Error fetching labels.'));
- });
+ const reqFailed = () => {
+ $dropdownToggle.data('bs.dropdown').hide();
+ flash(__('Error fetching labels.'));
+ };
+
+ if (store.getters.shouldUseGraphQL) {
+ store
+ .dispatch('fetchLabels')
+ .then((data) => callback(data))
+ .catch(reqFailed);
+ } else {
+ axios
+ .get($dropdownToggle.attr('data-list-labels-path'))
+ .then(({ data }) => callback(data))
+ .catch(reqFailed);
+ }
},
renderRow(label) {
- const active = boardsStore.findListByLabelId(label.id);
+ const active = store.getters.shouldUseGraphQL
+ ? store.getters.getListByLabelId(label.id)
+ : boardsStore.findListByLabelId(label.id);
const $li = $('<li />');
const $a = $('<a />', {
class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '',
@@ -87,7 +98,7 @@ export default function initNewListDropdown() {
e.preventDefault();
if (shouldCreateListGraphQL(label)) {
- store.dispatch('createList', { labelId: fullLabelId(label) });
+ store.dispatch('createList', { labelId: label.id });
} else if (!boardsStore.findListByLabelId(label.id)) {
boardsStore.new({
title: label.title,
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 04699d0d3a4..cfc1752a828 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapState } from 'vuex';
import {
GlDropdown,
GlDropdownItem,
@@ -8,6 +7,7 @@ import {
GlIntersectionObserver,
GlLoadingIcon,
} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
import { ListType } from '../constants';
diff --git a/app/assets/javascripts/boards/components/project_select_deprecated.vue b/app/assets/javascripts/boards/components/project_select_deprecated.vue
index a043dc575ca..5605e9945ea 100644
--- a/app/assets/javascripts/boards/components/project_select_deprecated.vue
+++ b/app/assets/javascripts/boards/components/project_select_deprecated.vue
@@ -6,11 +6,11 @@ import {
GlSearchBoxByType,
GlLoadingIcon,
} from '@gitlab/ui';
-import eventHub from '../eventhub';
import { s__ } from '~/locale';
-import Api from '../../api';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
+import Api from '../../api';
import { ListType } from '../constants';
+import eventHub from '../eventhub';
export default {
name: 'ProjectSelect',
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
index 4a664d5beef..6d928337396 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
@@ -1,9 +1,9 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import { GlButton, GlDatepicker } from '@gitlab/ui';
+import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import createFlash from '~/flash';
+import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
export default {
@@ -88,15 +88,13 @@ export default {
</gl-button>
</div>
</template>
- <template>
- <gl-datepicker
- ref="datePicker"
- :value="parsedDueDate"
- show-clear-button
- @input="setDueDate"
- @clear="setDueDate(null)"
- />
- </template>
+ <gl-datepicker
+ ref="datePicker"
+ :value="parsedDueDate"
+ show-clear-button
+ @input="setDueDate"
+ @clear="setDueDate(null)"
+ />
</board-editable-item>
</template>
<style>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
index d0e641daf5c..95864bd62a7 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
@@ -1,11 +1,11 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
-import { joinPaths } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
+import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
export default {
components: {
@@ -136,36 +136,34 @@ export default {
<template #collapsed>
<span class="gl-text-gray-800">{{ issue.referencePath }}</span>
</template>
- <template>
- <gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
- {{ $options.i18n.reviewYourChanges }}
- </gl-alert>
- <gl-form @submit.prevent="setTitle">
- <gl-form-group :invalid-feedback="$options.i18n.invalidFeedback" :state="validationState">
- <gl-form-input
- v-model="title"
- v-autofocusonshow
- :placeholder="$options.i18n.issueTitlePlaceholder"
- :state="validationState"
- />
- </gl-form-group>
+ <gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
+ {{ $options.i18n.reviewYourChanges }}
+ </gl-alert>
+ <gl-form @submit.prevent="setTitle">
+ <gl-form-group :invalid-feedback="$options.i18n.invalidFeedback" :state="validationState">
+ <gl-form-input
+ v-model="title"
+ v-autofocusonshow
+ :placeholder="$options.i18n.issueTitlePlaceholder"
+ :state="validationState"
+ />
+ </gl-form-group>
- <div class="gl-display-flex gl-w-full gl-justify-content-space-between gl-mt-5">
- <gl-button
- variant="success"
- size="small"
- data-testid="submit-button"
- :disabled="!title"
- @click="setTitle"
- >
- {{ $options.i18n.submitButton }}
- </gl-button>
+ <div class="gl-display-flex gl-w-full gl-justify-content-space-between gl-mt-5">
+ <gl-button
+ variant="success"
+ size="small"
+ data-testid="submit-button"
+ :disabled="!title"
+ @click="setTitle"
+ >
+ {{ $options.i18n.submitButton }}
+ </gl-button>
- <gl-button size="small" data-testid="cancel-button" @click="cancel">
- {{ $options.i18n.cancelButton }}
- </gl-button>
- </div>
- </gl-form>
- </template>
+ <gl-button size="small" data-testid="cancel-button" @click="cancel">
+ {{ $options.i18n.cancelButton }}
+ </gl-button>
+ </div>
+ </gl-form>
</board-editable-item>
</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index dcf769e6fe5..55b1596ee18 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -1,12 +1,12 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import { GlLabel } from '@gitlab/ui';
-import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import { isScopedLabel } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
export default {
components: {
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
index 144a81f009b..829f1c72806 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
@@ -1,5 +1,4 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import {
GlDropdown,
GlDropdownItem,
@@ -8,11 +7,11 @@ import {
GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
-import { fetchPolicies } from '~/lib/graphql';
+import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import groupMilestones from '../../graphql/group_milestones.query.graphql';
import createFlash from '~/flash';
import { __, s__ } from '~/locale';
+import projectMilestones from '../../graphql/project_milestones.query.graphql';
export default {
components: {
@@ -34,22 +33,21 @@ export default {
},
apollo: {
milestones: {
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
- query: groupMilestones,
+ query: projectMilestones,
debounce: 250,
skip() {
return !this.edit;
},
variables() {
return {
- fullPath: this.groupFullPath,
+ fullPath: this.projectPath,
searchTitle: this.searchTitle,
state: 'active',
- includeDescendants: true,
+ includeAncestors: true,
};
},
update(data) {
- const edges = data?.group?.milestones?.edges ?? [];
+ const edges = data?.project?.milestones?.edges ?? [];
return edges.map((item) => item.node);
},
error() {
@@ -74,21 +72,20 @@ export default {
return this.activeIssue.milestone?.title ?? this.$options.i18n.noMilestone;
},
},
- mounted() {
- this.$root.$on('bv::dropdown::hide', () => {
- this.$refs.sidebarItem.collapse();
- });
- },
methods: {
...mapActions(['setActiveIssueMilestone']),
handleOpen() {
this.edit = true;
this.$refs.dropdown.show();
},
+ handleClose() {
+ this.edit = false;
+ this.$refs.sidebarItem.collapse();
+ },
async setMilestone(milestoneId) {
this.loading = true;
this.searchTitle = '';
- this.$refs.sidebarItem.collapse();
+ this.handleClose();
try {
const input = { milestoneId, projectPath: this.projectPath };
@@ -117,45 +114,44 @@ export default {
:title="$options.i18n.milestone"
:loading="loading"
@open="handleOpen()"
- @close="edit = false"
+ @close="handleClose"
>
<template v-if="hasMilestone" #collapsed>
<strong class="gl-text-gray-900">{{ activeIssue.milestone.title }}</strong>
</template>
- <template>
- <gl-dropdown
- ref="dropdown"
- :text="dropdownText"
- :header-text="$options.i18n.assignMilestone"
- block
+ <gl-dropdown
+ ref="dropdown"
+ :text="dropdownText"
+ :header-text="$options.i18n.assignMilestone"
+ block
+ @hide="handleClose"
+ >
+ <gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
+ <gl-dropdown-item
+ data-testid="no-milestone-item"
+ :is-check-item="true"
+ :is-checked="!activeIssue.milestone"
+ @click="setMilestone(null)"
>
- <gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
+ {{ $options.i18n.noMilestone }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-loading-icon v-if="$apollo.loading" class="gl-py-4" />
+ <template v-else-if="milestones.length > 0">
<gl-dropdown-item
- data-testid="no-milestone-item"
+ v-for="milestone in milestones"
+ :key="milestone.id"
:is-check-item="true"
- :is-checked="!activeIssue.milestone"
- @click="setMilestone(null)"
+ :is-checked="activeIssue.milestone && milestone.id === activeIssue.milestone.id"
+ data-testid="milestone-item"
+ @click="setMilestone(milestone.id)"
>
- {{ $options.i18n.noMilestone }}
+ {{ milestone.title }}
</gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-loading-icon v-if="$apollo.loading" class="gl-py-4" />
- <template v-else-if="milestones.length > 0">
- <gl-dropdown-item
- v-for="milestone in milestones"
- :key="milestone.id"
- :is-check-item="true"
- :is-checked="activeIssue.milestone && milestone.id === activeIssue.milestone.id"
- data-testid="milestone-item"
- @click="setMilestone(milestone.id)"
- >
- {{ milestone.title }}
- </gl-dropdown-item>
- </template>
- <gl-dropdown-text v-else data-testid="no-milestones-found">
- {{ $options.i18n.noMilestonesFound }}
- </gl-dropdown-text>
- </gl-dropdown>
- </template>
+ </template>
+ <gl-dropdown-text v-else data-testid="no-milestones-found">
+ {{ $options.i18n.noMilestonesFound }}
+ </gl-dropdown-text>
+ </gl-dropdown>
</board-editable-item>
</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
index 4aa8d2f55e4..aa4fdcf9a94 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import { GlToggle } from '@gitlab/ui';
+import { mapGetters, mapActions } from 'vuex';
import createFlash from '~/flash';
import { __, s__ } from '~/locale';
diff --git a/app/assets/javascripts/boards/components/toggle_focus.vue b/app/assets/javascripts/boards/components/toggle_focus.vue
new file mode 100644
index 00000000000..74805f8a681
--- /dev/null
+++ b/app/assets/javascripts/boards/components/toggle_focus.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { hide } from '~/tooltips';
+
+export default {
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ issueBoardsContentSelector: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isFullscreen: false,
+ };
+ },
+ methods: {
+ toggleFocusMode() {
+ hide(this.$refs.toggleFocusModeButton);
+
+ const issueBoardsContent = document.querySelector(this.issueBoardsContentSelector);
+ issueBoardsContent.classList.toggle('is-focused');
+
+ this.isFullscreen = !this.isFullscreen;
+ },
+ },
+ i18n: {
+ toggleFocusMode: __('Toggle focus mode'),
+ },
+};
+</script>
+
+<template>
+ <div class="board-extra-actions gl-ml-3 gl-display-flex gl-align-items-center">
+ <gl-button
+ ref="toggleFocusModeButton"
+ v-gl-tooltip
+ :icon="isFullscreen ? 'minimize' : 'maximize'"
+ class="js-focus-mode-btn"
+ data-qa-selector="focus_mode_button"
+ :title="$options.i18n.toggleFocusMode"
+ @click="toggleFocusMode"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 9264fac5eda..3ab89b2c9da 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const BoardType = {
project: 'project',
group: 'group',
@@ -6,16 +8,34 @@ export const BoardType = {
export const ListType = {
assignee: 'assignee',
milestone: 'milestone',
+ iteration: 'iteration',
backlog: 'backlog',
closed: 'closed',
label: 'label',
};
+export const ListTypeTitles = {
+ assignee: __('Assignee'),
+ milestone: __('Milestone'),
+ iteration: __('Iteration'),
+ label: __('Label'),
+};
+
+export const formType = {
+ new: 'new',
+ delete: 'delete',
+ edit: 'edit',
+};
+
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
export const LIST = 'list';
+export const NOT_FILTER = 'not[';
+
+export const flashAnimationDuration = 2000;
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 94b35aadaf1..66580bdd30f 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,10 +1,10 @@
-import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
-import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
import { transformBoardConfig } from 'ee_else_ce/boards/boards_util';
+import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import { updateHistory } from '~/lib/utils/url_utility';
import FilteredSearchContainer from '../filtered_search/container';
-import boardsStore from './stores/boards_store';
import vuexstore from './stores';
-import { updateHistory } from '~/lib/utils/url_utility';
+import boardsStore from './stores/boards_store';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js
index c35dedde71b..1745ab3bab4 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import dateFormat from 'dateformat';
+import Vue from 'vue';
Vue.filter('due-date', (value) => {
const date = new Date(value);
diff --git a/app/assets/javascripts/boards/graphql/group_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
index f2ab12ef4a7..776530ebb83 100644
--- a/app/assets/javascripts/boards/graphql/group_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
@@ -1,11 +1,11 @@
query groupMilestones(
$fullPath: ID!
$state: MilestoneStateEnum
- $includeDescendants: Boolean
+ $includeAncestors: Boolean
$searchTitle: String
) {
- group(fullPath: $fullPath) {
- milestones(state: $state, includeDescendants: $includeDescendants, searchTitle: $searchTitle) {
+ project(fullPath: $fullPath) {
+ milestones(state: $state, includeAncestors: $includeAncestors, searchTitle: $searchTitle) {
edges {
node {
id
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index ef70a094f7c..859295318ed 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { mapActions, mapGetters } from 'vuex';
import 'ee_else_ce/boards/models/issue';
@@ -6,41 +7,39 @@ import 'ee_else_ce/boards/models/list';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
-import toggleLabels from 'ee_else_ce/boards/toggle_labels';
-import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import {
setWeightFetchingState,
setEpicFetchingState,
getMilestoneTitle,
getBoardsModalData,
} from 'ee_else_ce/boards/ee_functions';
-
-import VueApollo from 'vue-apollo';
+import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
+import toggleLabels from 'ee_else_ce/boards/toggle_labels';
+import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardExtraActions from '~/boards/components/board_extra_actions.vue';
-import createDefaultClient from '~/lib/graphql';
-import { deprecatedCreateFlash as Flash } from '~/flash';
-import { __ } from '~/locale';
import './models/label';
import './models/assignee';
-
-import toggleFocusMode from '~/boards/toggle_focus';
-import FilteredSearchBoards from '~/boards/filtered_search_boards';
-import eventHub from '~/boards/eventhub';
-import sidebarEventHub from '~/sidebar/event_hub';
import '~/boards/models/milestone';
import '~/boards/models/project';
+import '~/boards/filters/due_date_filters';
+import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
+import eventHub from '~/boards/eventhub';
+import FilteredSearchBoards from '~/boards/filtered_search_boards';
+import modalMixin from '~/boards/mixins/modal_mixins';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import ModalStore from '~/boards/stores/modal_store';
-import modalMixin from '~/boards/mixins/modal_mixins';
-import '~/boards/filters/due_date_filters';
-import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
+import toggleFocusMode from '~/boards/toggle_focus';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import createDefaultClient from '~/lib/graphql';
import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
} from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import sidebarEventHub from '~/sidebar/event_hub';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
Vue.use(VueApollo);
@@ -73,6 +72,7 @@ export default () => {
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
}
+ // eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({
el: $boardApp,
components: {
@@ -86,7 +86,7 @@ export default () => {
groupId: Number($boardApp.dataset.groupId),
rootPath: $boardApp.dataset.rootPath,
currentUserId: gon.current_user_id || null,
- canUpdate: $boardApp.dataset.canUpdate,
+ canUpdate: parseBoolean($boardApp.dataset.canUpdate),
labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath,
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
@@ -275,7 +275,7 @@ export default () => {
},
});
- // eslint-disable-next-line no-new
+ // eslint-disable-next-line no-new, @gitlab/no-runtime-template-compiler
new Vue({
el: document.getElementById('js-add-list'),
data: {
@@ -287,6 +287,21 @@ export default () => {
},
});
+ const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
+ if (createColumnTriggerEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: createColumnTriggerEl,
+ components: {
+ BoardAddNewColumnTrigger,
+ },
+ store,
+ render(createElement) {
+ return createElement('board-add-new-column-trigger');
+ },
+ });
+ }
+
boardConfigToggle(boardsStore);
const issueBoardsModal = document.getElementById('js-add-issues-btn');
@@ -341,5 +356,6 @@ export default () => {
mountMultipleBoardsSwitcher({
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
+ recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
});
};
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 1e77326ba9c..46d1239457d 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -6,8 +6,8 @@
import axios from '~/lib/utils/axios_utils';
import './label';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import IssueProject from './project';
import boardsStore from '../stores/boards_store';
+import IssueProject from './project';
class ListIssue {
constructor(obj) {
@@ -53,6 +53,10 @@ class ListIssue {
return boardsStore.findIssueAssignee(this, findAssignee);
}
+ setAssignees(assignees) {
+ boardsStore.setIssueAssignees(this, assignees);
+ }
+
removeAssignee(removeAssignee) {
boardsStore.removeIssueAssignee(this, removeAssignee);
}
diff --git a/app/assets/javascripts/boards/models/iteration.js b/app/assets/javascripts/boards/models/iteration.js
new file mode 100644
index 00000000000..b7bdc204f7c
--- /dev/null
+++ b/app/assets/javascripts/boards/models/iteration.js
@@ -0,0 +1,9 @@
+export default class ListIteration {
+ constructor(obj) {
+ this.id = obj.id;
+ this.title = obj.title;
+ this.state = obj.state;
+ this.webUrl = obj.web_url || obj.webUrl;
+ this.description = obj.description;
+ }
+}
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index be02ac7b889..6c6e2522d92 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,9 +1,10 @@
/* eslint-disable class-methods-use-this */
-import { __ } from '~/locale';
-import ListLabel from './label';
-import ListAssignee from './assignee';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { __ } from '~/locale';
import boardsStore from '../stores/boards_store';
+import ListAssignee from './assignee';
+import ListIteration from './iteration';
+import ListLabel from './label';
import ListMilestone from './milestone';
import 'ee_else_ce/boards/models/issue';
@@ -43,6 +44,7 @@ class List {
this.isExpandable = Boolean(typeInfo.isExpandable);
this.isExpanded = !obj.collapsed;
this.page = 1;
+ this.highlighted = obj.highlighted;
this.loading = true;
this.loadingMore = false;
this.issues = obj.issues || [];
@@ -57,6 +59,9 @@ class List {
} else if (IS_EE && obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
this.title = this.milestone.title;
+ } else if (IS_EE && obj.iteration) {
+ this.iteration = new ListIteration(obj.iteration);
+ this.title = this.iteration.title;
}
// doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index 738c8fb927e..fa58af24ba2 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -1,8 +1,12 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { mapGetters } from 'vuex';
+import BoardsSelector from '~/boards/components/boards_selector.vue';
+import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue';
+import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
-import BoardsSelector from '~/boards/components/boards_selector.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
Vue.use(VueApollo);
@@ -16,11 +20,15 @@ export default (params = {}) => {
el: boardsSwitcherElement,
components: {
BoardsSelector,
+ BoardsSelectorDeprecated,
},
+ mixins: [glFeatureFlagMixin()],
apolloProvider,
+ store,
provide: {
fullPath: params.fullPath,
rootPath: params.rootPath,
+ recentBoardsEndpoint: params.recentBoardsEndpoint,
},
data() {
const { dataset } = boardsSwitcherElement;
@@ -39,8 +47,16 @@ export default (params = {}) => {
return { boardsSelectorProps };
},
+ computed: {
+ ...mapGetters(['shouldUseGraphQL']),
+ },
render(createElement) {
- return createElement(BoardsSelector, {
+ if (this.shouldUseGraphQL) {
+ return createElement(BoardsSelector, {
+ props: this.boardsSelectorProps,
+ });
+ }
+ return createElement(BoardsSelectorDeprecated, {
props: this.boardsSelectorProps,
});
},
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 1d34f21798a..a7cf1e9e647 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,11 +1,9 @@
import { pick } from 'lodash';
-
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
-import createGqClient, { fetchPolicies } from '~/lib/graphql';
+import { BoardType, ListType, inactiveId, flashAnimationDuration } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
-import { BoardType, ListType, inactiveId } from '~/boards/constants';
-import * as types from './mutation_types';
import {
formatBoardLists,
formatListIssues,
@@ -14,23 +12,22 @@ import {
formatIssue,
formatIssueInput,
updateListPosition,
+ transformNotFilters,
} from '../boards_util';
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
-import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import createBoardListMutation from '../graphql/board_list_create.mutation.graphql';
-import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql';
-import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql';
+import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql';
+import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
-import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
+import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
-import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
+import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
+import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
import issueSetTitleMutation from '../graphql/issue_set_title.mutation.graphql';
-import groupProjectsQuery from '../graphql/group_projects.query.graphql';
+import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
+import * as types from './mutation_types';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -66,6 +63,7 @@ export default {
'releaseTag',
'search',
]);
+ filterParams.not = transformNotFilters(filters);
commit(types.SET_FILTERS, filterParams);
},
@@ -108,9 +106,31 @@ export default {
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
},
- createList: ({ state, commit, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
+ highlightList: ({ commit, state }, listId) => {
+ if ([ListType.backlog, ListType.closed].includes(state.boardLists[listId].listType)) {
+ return;
+ }
+
+ commit(types.ADD_LIST_TO_HIGHLIGHTED_LISTS, listId);
+
+ setTimeout(() => {
+ commit(types.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS, listId);
+ }, flashAnimationDuration);
+ },
+
+ createList: (
+ { state, commit, dispatch, getters },
+ { backlog, labelId, milestoneId, assigneeId },
+ ) => {
const { boardId } = state;
+ const existingList = getters.getListByLabelId(labelId);
+
+ if (existingList) {
+ dispatch('highlightList', existingList.id);
+ return;
+ }
+
gqlClient
.mutate({
mutation: createBoardListMutation,
@@ -128,6 +148,7 @@ export default {
} else {
const list = data.boardListCreate?.list;
dispatch('addList', list);
+ dispatch('highlightList', list.id);
}
})
.catch(() => commit(types.CREATE_LIST_FAILURE));
@@ -153,10 +174,10 @@ export default {
variables,
})
.then(({ data }) => {
- const labels = data[boardType]?.labels;
- return labels.nodes;
- })
- .catch(() => commit(types.RECEIVE_LABELS_FAILURE));
+ const labels = data[boardType]?.labels.nodes;
+ commit(types.RECEIVE_LABELS_SUCCESS, labels);
+ return labels;
+ });
},
moveList: (
@@ -308,34 +329,11 @@ export default {
},
setAssignees: ({ commit, getters }, assigneeUsernames) => {
- commit(types.SET_ASSIGNEE_LOADING, true);
-
- return gqlClient
- .mutate({
- mutation: updateAssigneesMutation,
- variables: {
- iid: getters.activeIssue.iid,
- projectPath: getters.activeIssue.referencePath.split('#')[0],
- assigneeUsernames,
- },
- })
- .then(({ data }) => {
- const { nodes } = data.issueSetAssignees?.issue?.assignees || [];
-
- commit('UPDATE_ISSUE_BY_ID', {
- issueId: getters.activeIssue.id,
- prop: 'assignees',
- value: nodes,
- });
-
- return nodes;
- })
- .catch(() => {
- createFlash({ message: __('An error occurred while updating assignees.') });
- })
- .finally(() => {
- commit(types.SET_ASSIGNEE_LOADING, false);
- });
+ commit('UPDATE_ISSUE_BY_ID', {
+ issueId: getters.activeIssue.id,
+ prop: 'assignees',
+ value: assigneeUsernames,
+ });
},
setActiveIssueMilestone: async ({ commit, getters }, input) => {
@@ -534,6 +532,21 @@ export default {
commit(types.SET_SELECTED_PROJECT, project);
},
+ toggleBoardItemMultiSelection: ({ commit, state }, boardItem) => {
+ const { selectedBoardItems } = state;
+ const index = selectedBoardItems.indexOf(boardItem);
+
+ if (index === -1) {
+ commit(types.ADD_BOARD_ITEM_TO_SELECTION, boardItem);
+ } else {
+ commit(types.REMOVE_BOARD_ITEM_FROM_SELECTION, boardItem);
+ }
+ },
+
+ setAddColumnFormVisibility: ({ commit }, visible) => {
+ commit(types.SET_ADD_COLUMN_FORM_VISIBLE, visible);
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index f59530ddf8f..fbff736c7e1 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -4,22 +4,22 @@
import { sortBy } from 'lodash';
import Vue from 'vue';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import createDefaultClient from '~/lib/graphql';
+import axios from '~/lib/utils/axios_utils';
import {
urlParamsToObject,
getUrlParamsArray,
parseBoolean,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
-import createDefaultClient from '~/lib/graphql';
-import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ListType, flashAnimationDuration } from '../constants';
import eventHub from '../eventhub';
-import { ListType } from '../constants';
-import IssueProject from '../models/project';
-import ListLabel from '../models/label';
import ListAssignee from '../models/assignee';
+import ListLabel from '../models/label';
import ListMilestone from '../models/milestone';
+import IssueProject from '../models/project';
const PER_PAGE = 20;
export const gqlClient = createDefaultClient();
@@ -106,6 +106,11 @@ const boardsStore = {
list
.save()
.then(() => {
+ list.highlighted = true;
+ setTimeout(() => {
+ list.highlighted = false;
+ }, flashAnimationDuration);
+
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
@@ -117,7 +122,6 @@ const boardsStore = {
},
updateNewListDropdown(listId) {
- // eslint-disable-next-line no-unused-expressions
document
.querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`)
?.classList.remove('is-active');
@@ -720,6 +724,10 @@ const boardsStore = {
}
},
+ setIssueAssignees(issue, assignees) {
+ issue.assignees = [...assignees];
+ },
+
removeIssueLabels(issue, labels) {
labels.forEach(issue.removeLabel.bind(issue));
},
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index d72b5c6fb8e..cab97088bc6 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -17,12 +17,20 @@ export default {
return state.issues[state.activeId] || {};
},
+ groupPathForActiveIssue: (_, getters) => {
+ const { referencePath = '' } = getters.activeIssue;
+ return referencePath.slice(0, referencePath.indexOf('/'));
+ },
+
projectPathForActiveIssue: (_, getters) => {
- const referencePath = getters.activeIssue.referencePath || '';
+ const { referencePath = '' } = getters.activeIssue;
return referencePath.slice(0, referencePath.indexOf('#'));
},
getListByLabelId: (state) => (labelId) => {
+ if (!labelId) {
+ return null;
+ }
return find(state.boardLists, (l) => l.label?.id === labelId);
},
diff --git a/app/assets/javascripts/boards/stores/index.js b/app/assets/javascripts/boards/stores/index.js
index 471b952a212..0a87c6ab821 100644
--- a/app/assets/javascripts/boards/stores/index.js
+++ b/app/assets/javascripts/boards/stores/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import state from 'ee_else_ce/boards/stores/state';
-import getters from 'ee_else_ce/boards/stores/getters';
import actions from 'ee_else_ce/boards/stores/actions';
+import getters from 'ee_else_ce/boards/stores/getters';
import mutations from 'ee_else_ce/boards/stores/mutations';
+import state from 'ee_else_ce/boards/stores/state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 4697f39498a..a89e961ae2d 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -2,7 +2,7 @@ export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const SET_FILTERS = 'SET_FILTERS';
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
-export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
+export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE';
export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
@@ -40,3 +40,8 @@ export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
export const RECEIVE_GROUP_PROJECTS_SUCCESS = 'RECEIVE_GROUP_PROJECTS_SUCCESS';
export const RECEIVE_GROUP_PROJECTS_FAILURE = 'RECEIVE_GROUP_PROJECTS_FAILURE';
export const SET_SELECTED_PROJECT = 'SET_SELECTED_PROJECT';
+export const ADD_BOARD_ITEM_TO_SELECTION = 'ADD_BOARD_ITEM_TO_SELECTION';
+export const REMOVE_BOARD_ITEM_FROM_SELECTION = 'REMOVE_BOARD_ITEM_FROM_SELECTION';
+export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE';
+export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
+export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 6c79b22d308..79c98c3d90c 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,9 +1,9 @@
-import Vue from 'vue';
import { pull, union } from 'lodash';
+import Vue from 'vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { s__ } from '~/locale';
import { formatIssue, moveIssueListHelper } from '../boards_util';
import * as mutationTypes from './mutation_types';
-import { s__ } from '~/locale';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -63,8 +63,8 @@ export default {
state.error = s__('Boards|An error occurred while creating the list. Please try again.');
},
- [mutationTypes.RECEIVE_LABELS_FAILURE]: (state) => {
- state.error = s__('Boards|An error occurred while fetching labels. Please reload the page.');
+ [mutationTypes.RECEIVE_LABELS_SUCCESS]: (state, labels) => {
+ state.labels = labels;
},
[mutationTypes.GENERATE_DEFAULT_LISTS_FAILURE]: (state) => {
@@ -258,4 +258,28 @@ export default {
[mutationTypes.SET_SELECTED_PROJECT]: (state, project) => {
state.selectedProject = project;
},
+
+ [mutationTypes.ADD_BOARD_ITEM_TO_SELECTION]: (state, boardItem) => {
+ state.selectedBoardItems = [...state.selectedBoardItems, boardItem];
+ },
+
+ [mutationTypes.REMOVE_BOARD_ITEM_FROM_SELECTION]: (state, boardItem) => {
+ Vue.set(
+ state,
+ 'selectedBoardItems',
+ state.selectedBoardItems.filter((obj) => obj !== boardItem),
+ );
+ },
+
+ [mutationTypes.SET_ADD_COLUMN_FORM_VISIBLE]: (state, visible) => {
+ state.addColumnFormVisible = visible;
+ },
+
+ [mutationTypes.ADD_LIST_TO_HIGHLIGHTED_LISTS]: (state, listId) => {
+ state.highlightedLists.push(listId);
+ },
+
+ [mutationTypes.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS]: (state, listId) => {
+ state.highlightedLists = state.highlightedLists.filter((id) => id !== listId);
+ },
};
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index aba7da373cf..91544d6c9c5 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -14,6 +14,9 @@ export default () => ({
issues: {},
filterParams: {},
boardConfig: {},
+ labels: [],
+ highlightedLists: [],
+ selectedBoardItems: [],
groupProjects: [],
groupProjectsFlags: {
isLoading: false,
@@ -22,6 +25,7 @@ export default () => ({
},
selectedProject: {},
error: undefined,
+ addColumnFormVisible: false,
// TODO: remove after ce/ee split of board_content.vue
isShowingEpicsSwimlanes: false,
});
diff --git a/app/assets/javascripts/boards/toggle_focus.js b/app/assets/javascripts/boards/toggle_focus.js
index 347deb81846..0a230f72dcc 100644
--- a/app/assets/javascripts/boards/toggle_focus.js
+++ b/app/assets/javascripts/boards/toggle_focus.js
@@ -1,45 +1,17 @@
-import $ from 'jquery';
import Vue from 'vue';
-import { GlIcon } from '@gitlab/ui';
-import { hide } from '~/tooltips';
+import ToggleFocus from './components/toggle_focus.vue';
-export default (ModalStore, boardsStore) => {
- const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board');
+export default () => {
+ const issueBoardsContentSelector = '.content-wrapper > .js-focus-mode-board';
return new Vue({
- el: document.getElementById('js-toggle-focus-btn'),
- components: {
- GlIcon,
+ el: '#js-toggle-focus-btn',
+ render(h) {
+ return h(ToggleFocus, {
+ props: {
+ issueBoardsContentSelector,
+ },
+ });
},
- data: {
- modal: ModalStore.store,
- store: boardsStore.state,
- isFullscreen: false,
- },
- methods: {
- toggleFocusMode() {
- const $el = $(this.$refs.toggleFocusModeButton);
- hide($el);
-
- issueBoardsContent.classList.toggle('is-focused');
-
- this.isFullscreen = !this.isFullscreen;
- },
- },
- template: `
- <div class="board-extra-actions">
- <a
- href="#"
- class="btn btn-default has-tooltip gl-ml-3 js-focus-mode-btn"
- data-qa-selector="focus_mode_button"
- role="button"
- aria-label="Toggle focus mode"
- title="Toggle focus mode"
- ref="toggleFocusModeButton"
- @click="toggleFocusMode">
- <gl-icon :name="isFullscreen ? 'minimize' : 'maximize'" />
- </a>
- </div>
- `,
});
};
diff --git a/app/assets/javascripts/branches/components/divergence_graph.vue b/app/assets/javascripts/branches/components/divergence_graph.vue
index deaed694b46..c4b522a43d4 100644
--- a/app/assets/javascripts/branches/components/divergence_graph.vue
+++ b/app/assets/javascripts/branches/components/divergence_graph.vue
@@ -1,7 +1,7 @@
<script>
import { sprintf, __ } from '~/locale';
-import GraphBar from './graph_bar.vue';
import { MAX_COMMIT_COUNT } from '../constants';
+import GraphBar from './graph_bar.vue';
export default {
components: {
diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js
index a577bdca082..ca019bc4178 100644
--- a/app/assets/javascripts/branches/divergence_graph.js
+++ b/app/assets/javascripts/branches/divergence_graph.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
-import { __ } from '../locale';
import { deprecatedCreateFlash as createFlash } from '../flash';
import axios from '../lib/utils/axios_utils';
+import { __ } from '../locale';
import DivergenceGraph from './components/divergence_graph.vue';
export function createGraphVueApp(el, data, maxCommits) {
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index 42e0f8b37bd..e895df01f2c 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,9 +1,9 @@
/* eslint-disable func-names */
import $ from 'jquery';
-import { visitUrl } from './lib/utils/url_utility';
-import { parseBoolean } from './lib/utils/common_utils';
import { hide, initTooltips, show } from '~/tooltips';
+import { parseBoolean } from './lib/utils/common_utils';
+import { visitUrl } from './lib/utils/url_utility';
export default class BuildArtifacts {
constructor() {
diff --git a/app/assets/javascripts/captcha/captcha_modal.vue b/app/assets/javascripts/captcha/captcha_modal.vue
new file mode 100644
index 00000000000..e6c73bc9643
--- /dev/null
+++ b/app/assets/javascripts/captcha/captcha_modal.vue
@@ -0,0 +1,110 @@
+<script>
+// NOTE 1: This is similar to recaptcha_modal.vue, but it directly uses the reCAPTCHA Javascript API
+// (https://developers.google.com/recaptcha/docs/display#js_api) and gl-modal, rather than relying
+// on the form-based ReCAPTCHA HTML being pre-rendered by the backend and using deprecated-modal.
+
+// NOTE 2: Even though this modal currently only supports reCAPTCHA, we use 'captcha' instead
+// of 'recaptcha' throughout the code, so that we can easily add support for future alternative
+// captcha implementations other than reCAPTCHA (e.g. FriendlyCaptcha) without having to
+// change the references in the code or API.
+
+import { GlModal } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { initRecaptchaScript } from '~/captcha/init_recaptcha_script';
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ needsCaptchaResponse: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ captchaSiteKey: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ modalId: uniqueId('captcha-modal-'),
+ };
+ },
+ watch: {
+ needsCaptchaResponse(newNeedsCaptchaResponse) {
+ // If this is true, we need to present the captcha modal to the user.
+ // When the modal is shown we will also initialize and render the form.
+ if (newNeedsCaptchaResponse) {
+ this.$refs.modal.show();
+ }
+ },
+ },
+ methods: {
+ emitReceivedCaptchaResponse(captchaResponse) {
+ this.$emit('receivedCaptchaResponse', captchaResponse);
+ this.$refs.modal.hide();
+ },
+ emitNullReceivedCaptchaResponse() {
+ this.emitReceivedCaptchaResponse(null);
+ },
+ /**
+ * handler for when modal is shown
+ */
+ shown() {
+ const containerRef = this.$refs.captcha;
+
+ // NOTE: This is the only bit that is specific to Google's reCAPTCHA captcha implementation.
+ initRecaptchaScript()
+ .then((grecaptcha) => {
+ grecaptcha.render(containerRef, {
+ sitekey: this.captchaSiteKey,
+ // This callback will emit and let the parent handle the response
+ callback: this.emitReceivedCaptchaResponse,
+ // TODO: Also need to handle expired-callback and error-callback
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/217722#future-follow-on-issuesmrs
+ });
+ })
+ .catch((e) => {
+ // TODO: flash the error or notify the user some other way
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/217722#future-follow-on-issuesmrs
+ this.emitNullReceivedCaptchaResponse();
+ this.$refs.modal.hide();
+
+ // eslint-disable-next-line no-console
+ console.error(
+ '[gitlab] an unexpected exception was caught while initializing captcha',
+ e,
+ );
+ });
+ },
+ /**
+ * handler for when modal is about to hide
+ */
+ hide(bvModalEvent) {
+ // If hide() was called without any argument, the value of trigger will be null.
+ // See https://bootstrap-vue.org/docs/components/modal#prevent-closing
+ if (bvModalEvent.trigger) {
+ this.emitNullReceivedCaptchaResponse();
+ }
+ },
+ },
+};
+</script>
+<template>
+ <!-- Note: The action-cancel button isn't necessary for the functionality of the modal, but -->
+ <!-- there must be at least one button or focusable element, or the gl-modal fails to render. -->
+ <!-- We could modify gl-model to remove this requirement. -->
+ <gl-modal
+ ref="modal"
+ :modal-id="modalId"
+ :title="__('Please solve the captcha')"
+ :action-cancel="{ text: __('Cancel') }"
+ @shown="shown"
+ @hide="hide"
+ >
+ <div ref="captcha"></div>
+ <p>{{ __('We want to be sure it is you, please confirm you are not a robot.') }}</p>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/captcha/init_recaptcha_script.js b/app/assets/javascripts/captcha/init_recaptcha_script.js
new file mode 100644
index 00000000000..f546eef7d84
--- /dev/null
+++ b/app/assets/javascripts/captcha/init_recaptcha_script.js
@@ -0,0 +1,48 @@
+// NOTE: This module will be used in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52044
+import { memoize } from 'lodash';
+
+export const RECAPTCHA_API_URL_PREFIX = 'https://www.google.com/recaptcha/api.js';
+export const RECAPTCHA_ONLOAD_CALLBACK_NAME = 'recaptchaOnloadCallback';
+
+/**
+ * Adds the Google reCAPTCHA script tag to the head of the document, and
+ * returns a promise of the grecaptcha object
+ * (https://developers.google.com/recaptcha/docs/display#js_api).
+ *
+ * It is memoized, so there will only be one instance of the script tag ever
+ * added to the document.
+ *
+ * See the reCAPTCHA documentation for more details:
+ *
+ * https://developers.google.com/recaptcha/docs/display#explicit_render
+ *
+ */
+export const initRecaptchaScript = memoize(() => {
+ // Appends the the reCAPTCHA script tag to the head of document
+ const appendRecaptchaScript = () => {
+ const script = document.createElement('script');
+ script.src = `${RECAPTCHA_API_URL_PREFIX}?onload=${RECAPTCHA_ONLOAD_CALLBACK_NAME}&render=explicit`;
+ script.classList.add('js-recaptcha-script');
+ document.head.appendChild(script);
+ };
+
+ return new Promise((resolve) => {
+ // This global callback resolves the Promise and is passed by name to the reCAPTCHA script.
+ window[RECAPTCHA_ONLOAD_CALLBACK_NAME] = () => {
+ // Let's clean up after ourselves. This is also important for testing, because `window` is NOT cleared between tests.
+ // https://github.com/facebook/jest/issues/1224#issuecomment-444586798.
+ delete window[RECAPTCHA_ONLOAD_CALLBACK_NAME];
+ resolve(window.grecaptcha);
+ };
+ appendRecaptchaScript();
+ });
+});
+
+/**
+ * Clears the cached memoization of the default manager.
+ *
+ * This is needed for determinism in tests.
+ */
+export const clearMemoizeCache = () => {
+ initRecaptchaScript.cache.clear();
+};
diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci_lint/components/ci_lint.vue
index fc47fe8c333..9a55177b15f 100644
--- a/app/assets/javascripts/ci_lint/components/ci_lint.vue
+++ b/app/assets/javascripts/ci_lint/components/ci_lint.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton, GlFormCheckbox, GlIcon, GlLink, GlAlert } from '@gitlab/ui';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
import lintCiMutation from '~/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql';
+import EditorLite from '~/vue_shared/components/editor_lite.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
index b84c188cd08..0233ffaccdc 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
@@ -2,9 +2,9 @@
import { GlTable, GlButton, GlBadge, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.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: {
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
index dc79bbb4d97..f2972133aad 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import TriggersList from './components/triggers_list.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import TriggersList from './components/triggers_list.vue';
const parseJsonArray = (triggers) => {
try {
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index aa4d311527e..065cb4f5616 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
+import SecretValues from '../behaviors/secret_values';
+import CreateItemDropdown from '../create_item_dropdown';
import { parseBoolean } from '../lib/utils/common_utils';
import { s__ } from '../locale';
import setupToggleButtons from '../toggle_buttons';
-import CreateItemDropdown from '../create_item_dropdown';
-import SecretValues from '../behaviors/secret_values';
const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments');
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index da816f85466..af4349083b6 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -15,17 +15,17 @@ import {
} from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState } from 'vuex';
-import { mapComputed } from '~/vuex_shared/bindings';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { mapComputed } from '~/vuex_shared/bindings';
import {
AWS_TOKEN_CONSTANTS,
ADD_CI_VARIABLE_MODAL_ID,
AWS_TIP_DISMISSED_COOKIE_NAME,
AWS_TIP_MESSAGE,
} from '../constants';
-import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
+import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
export default {
modalId: ADD_CI_VARIABLE_MODAL_ID,
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
index 431819124c2..6e6527df63f 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
@@ -38,7 +38,9 @@ export default {
<template>
<div id="popover-container">
<gl-popover :target="target" triggers="hover" placement="top" container="popover-container">
- <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-word-break-all"
+ >
<div class="ci-popover-value gl-pr-3">
{{ displayValue }}
</div>
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index a28b52d6b57..37b5f7e6df7 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import CiVariableSettings from './components/ci_variable_settings.vue';
import createStore from './store';
-import { parseBoolean } from '~/lib/utils/common_utils';
export default (containerId = 'js-ci-project-variables') => {
const containerEl = document.getElementById(containerId);
diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js
index ac595fa0045..8569cecc6a7 100644
--- a/app/assets/javascripts/ci_variable_list/store/actions.js
+++ b/app/assets/javascripts/ci_variable_list/store/actions.js
@@ -1,8 +1,8 @@
-import * as types from './mutation_types';
-import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
+import * as types from './mutation_types';
import { prepareDataForApi, prepareDataForDisplay, prepareEnvironments } from './utils';
export const toggleValues = ({ commit }, valueState) => {
diff --git a/app/assets/javascripts/ci_variable_list/store/mutations.js b/app/assets/javascripts/ci_variable_list/store/mutations.js
index 961cecee298..0e7c61cecb8 100644
--- a/app/assets/javascripts/ci_variable_list/store/mutations.js
+++ b/app/assets/javascripts/ci_variable_list/store/mutations.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { displayText } from '../constants';
+import * as types from './mutation_types';
export default {
[types.REQUEST_VARIABLES](state) {
diff --git a/app/assets/javascripts/clone_panel.js b/app/assets/javascripts/clone_panel.js
index 00bf54e1478..c9fae8f17a4 100644
--- a/app/assets/javascripts/clone_panel.js
+++ b/app/assets/javascripts/clone_panel.js
@@ -18,6 +18,11 @@ export default function initClonePanel() {
e.preventDefault();
const $this = $(e.currentTarget);
const url = $this.attr('href');
+ if (url && (url.startsWith('vscode://') || url.startsWith('xcode://'))) {
+ // Clone with "..." should open like a normal link
+ return;
+ }
+ e.preventDefault();
const cloneType = $this.data('cloneType');
$('.is-active', $cloneOptions).removeClass('is-active');
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index eb2128b2856..5cb3d913210 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,20 +1,20 @@
+import { GlToast } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import Vue from 'vue';
-import { GlToast } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
-import PersistentUserCallout from '../persistent_user_callout';
-import { s__, sprintf } from '../locale';
+import initProjectSelectDropdown from '~/project_select';
+import initServerlessSurveyBanner from '~/serverless/survey_banner';
import { deprecatedCreateFlash as Flash } from '../flash';
import Poll from '../lib/utils/poll';
+import { s__, sprintf } from '../locale';
+import PersistentUserCallout from '../persistent_user_callout';
import initSettingsPanels from '../settings_panels';
-import eventHub from './event_hub';
+import Applications from './components/applications.vue';
+import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
import { APPLICATION_STATUS, CROSSPLANE, KNATIVE, FLUENTD } from './constants';
+import eventHub from './event_hub';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
-import Applications from './components/applications.vue';
-import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
-import initProjectSelectDropdown from '~/project_select';
-import initServerlessSurveyBanner from '~/serverless/survey_banner';
const Environments = () => import('ee_component/clusters/components/environments.vue');
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 471c1a0b4a2..76fe076d4ff 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -1,14 +1,13 @@
<script>
import { GlLink, GlModalDirective, GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
-import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
+import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
+import eventHub from '../event_hub';
import UninstallApplicationButton from './uninstall_application_button.vue';
import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from './update_application_confirmation_modal.vue';
-import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
-
export default {
components: {
GlButton,
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index e096a29ce7f..8ef3169dc65 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,23 +1,23 @@
<script>
import { GlLoadingIcon, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
+import certManagerLogo from 'images/cluster_app_logos/cert_manager.png';
+import crossplaneLogo from 'images/cluster_app_logos/crossplane.png';
+import elasticStackLogo from 'images/cluster_app_logos/elastic_stack.png';
+import fluentdLogo from 'images/cluster_app_logos/fluentd.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.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 crossplaneLogo from 'images/cluster_app_logos/crossplane.png';
import knativeLogo from 'images/cluster_app_logos/knative.png';
+import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
-import elasticStackLogo from 'images/cluster_app_logos/elastic_stack.png';
-import fluentdLogo from 'images/cluster_app_logos/fluentd.png';
-import applicationRow from './application_row.vue';
+import eventHub from '~/clusters/event_hub';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import KnativeDomainEditor from './knative_domain_editor.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
-import eventHub from '~/clusters/event_hub';
+import applicationRow from './application_row.vue';
import CrossplaneProviderStack from './crossplane_provider_stack.vue';
-import IngressModsecuritySettings from './ingress_modsecurity_settings.vue';
import FluentdOutputSettings from './fluentd_output_settings.vue';
+import IngressModsecuritySettings from './ingress_modsecurity_settings.vue';
+import KnativeDomainEditor from './knative_domain_editor.vue';
export default {
components: {
@@ -349,6 +349,7 @@ export default {
{{ s__('ClusterIntegration|Issuer Email') }}
</label>
<div class="input-group">
+ <!-- eslint-disable vue/no-mutating-props -->
<input
id="cert-manager-issuer-email"
v-model="applications.cert_manager.email"
@@ -356,6 +357,7 @@ export default {
type="text"
class="form-control js-email"
/>
+ <!-- eslint-enable vue/no-mutating-props -->
</div>
<p class="form-text text-muted">
{{
@@ -522,6 +524,7 @@ export default {
<label for="jupyter-hostname">{{ s__('ClusterIntegration|Jupyter Hostname') }}</label>
<div class="input-group">
+ <!-- eslint-disable vue/no-mutating-props -->
<input
id="jupyter-hostname"
v-model="applications.jupyter.hostname"
@@ -529,6 +532,7 @@ export default {
type="text"
class="form-control js-hostname"
/>
+ <!-- eslint-enable vue/no-mutating-props -->
<span class="input-group-append">
<clipboard-button
:text="jupyterHostname"
diff --git a/app/assets/javascripts/clusters/components/fluentd_output_settings.vue b/app/assets/javascripts/clusters/components/fluentd_output_settings.vue
index 84a39874000..369cb2fa0f3 100644
--- a/app/assets/javascripts/clusters/components/fluentd_output_settings.vue
+++ b/app/assets/javascripts/clusters/components/fluentd_output_settings.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert, GlButton, GlDropdown, GlDropdownItem, GlFormCheckbox } from '@gitlab/ui';
import { mapValues } from 'lodash';
-import { __ } from '~/locale';
import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
+import { __ } from '~/locale';
const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS;
diff --git a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
index f05c8db5d56..26767c32275 100644
--- a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
+++ b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
@@ -1,5 +1,4 @@
<script>
-import { escape } from 'lodash';
import {
GlAlert,
GlSprintf,
@@ -10,10 +9,11 @@ import {
GlDropdownItem,
GlIcon,
} from '@gitlab/ui';
+import { escape } from 'lodash';
import modSecurityLogo from 'images/cluster_app_logos/gitlab.png';
-import { s__, __ } from '../../locale';
import { APPLICATION_STATUS, INGRESS, LOGGING_MODE, BLOCKING_MODE } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
+import { s__, __ } from '../../locale';
const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS;
@@ -130,9 +130,11 @@ export default {
},
resetStatus() {
if (this.initialMode !== null) {
+ // eslint-disable-next-line vue/no-mutating-props
this.ingress.modsecurity_mode = this.initialMode;
}
if (this.initialValue !== null) {
+ // eslint-disable-next-line vue/no-mutating-props
this.ingress.modsecurity_enabled = this.initialValue;
}
this.initialValue = null;
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
index d80bd6f5b42..89446680173 100644
--- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -9,10 +9,10 @@ import {
GlButton,
GlAlert,
} from '@gitlab/ui';
-import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import { APPLICATION_STATUS } from '~/clusters/constants';
import { __, s__ } from '~/locale';
-import { APPLICATION_STATUS } from '~/clusters/constants';
+import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index c157b04b4f5..5cd9baf2c2b 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -1,10 +1,10 @@
<script>
/* eslint-disable vue/no-v-html */
-import { escape } from 'lodash';
import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
-import SplitButton from '~/vue_shared/components/split_button.vue';
-import { s__, sprintf } from '~/locale';
+import { escape } from 'lodash';
import csrf from '~/lib/utils/csrf';
+import { s__, sprintf } from '~/locale';
+import SplitButton from '~/vue_shared/components/split_button.vue';
const splitButtonActionItems = [
{
@@ -131,48 +131,46 @@ export default {
:title="modalTitle"
kind="danger"
>
- <template>
- <p>{{ warningMessage }}</p>
- <div v-if="confirmCleanup">
- {{ s__('ClusterIntegration|This will permanently delete the following resources:') }}
- <ul>
- <li>
- {{ s__('ClusterIntegration|All installed applications and related resources') }}
- </li>
- <li>
- <gl-sprintf :message="s__('ClusterIntegration|The %{gitlabNamespace} namespace')">
- <template #gitlabNamespace>
- <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
- <code>{{ 'gitlab-managed-apps' }}</code>
- </template>
- </gl-sprintf>
- </li>
- <li>{{ s__('ClusterIntegration|Any project namespaces') }}</li>
- <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
- <li><code>clusterroles</code></li>
- <li><code>clusterrolebindings</code></li>
- <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
- </ul>
- </div>
- <strong v-html="confirmationTextLabel"></strong>
- <form ref="form" :action="clusterPath" method="post" class="gl-mb-5">
- <input ref="method" type="hidden" name="_method" value="delete" />
- <input :value="csrfToken" type="hidden" name="authenticity_token" />
- <input ref="cleanup" type="hidden" name="cleanup" value="true" />
- <gl-form-input
- v-model="enteredClusterName"
- autofocus
- type="text"
- name="confirm_cluster_name_input"
- autocomplete="off"
- />
- </form>
- <span v-if="confirmCleanup">{{
- s__(
- 'ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration.',
- )
- }}</span>
- </template>
+ <p>{{ warningMessage }}</p>
+ <div v-if="confirmCleanup">
+ {{ s__('ClusterIntegration|This will permanently delete the following resources:') }}
+ <ul>
+ <li>
+ {{ s__('ClusterIntegration|All installed applications and related resources') }}
+ </li>
+ <li>
+ <gl-sprintf :message="s__('ClusterIntegration|The %{gitlabNamespace} namespace')">
+ <template #gitlabNamespace>
+ <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+ <code>{{ 'gitlab-managed-apps' }}</code>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>{{ s__('ClusterIntegration|Any project namespaces') }}</li>
+ <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
+ <li><code>clusterroles</code></li>
+ <li><code>clusterrolebindings</code></li>
+ <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
+ </ul>
+ </div>
+ <strong v-html="confirmationTextLabel"></strong>
+ <form ref="form" :action="clusterPath" method="post" class="gl-mb-5">
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <input ref="cleanup" type="hidden" name="cleanup" value="true" />
+ <gl-form-input
+ v-model="enteredClusterName"
+ autofocus
+ type="text"
+ name="confirm_cluster_name_input"
+ autocomplete="off"
+ />
+ </form>
+ <span v-if="confirmCleanup">{{
+ s__(
+ 'ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration.',
+ )
+ }}</span>
<template #modal-footer>
<gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button>
<template v-if="confirmCleanup">
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 5de487308c5..d45696c98c2 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 { parseBoolean } from '../../lib/utils/common_utils';
+import { s__ } from '../../locale';
import {
INGRESS,
JUPYTER,
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 53eec5c8a0d..8f81d967126 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -1,5 +1,4 @@
<script>
-import { mapState, mapActions } from 'vuex';
import {
GlBadge,
GlLink,
@@ -10,10 +9,11 @@ import {
GlTable,
GlTooltipDirective,
} from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { __, sprintf } from '~/locale';
+import { CLUSTER_TYPES, STATUSES } from '../constants';
import AncestorNotice from './ancestor_notice.vue';
import NodeErrorHelpText from './node_error_help_text.vue';
-import { CLUSTER_TYPES, STATUSES } from '../constants';
-import { __, sprintf } from '~/locale';
export default {
nodeMemoryText: __('%{totalMemory} (%{freeSpacePercentage}%{percentSymbol} free)'),
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 97ed0a7ab37..45d3e0cbc23 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -1,10 +1,10 @@
-import * as Sentry from '~/sentry/wrapper';
-import Poll from '~/lib/utils/poll';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
+import * as Sentry from '~/sentry/wrapper';
import { MAX_REQUESTS } from '../constants';
-import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
const allNodesPresent = (clusters, retryCount) => {
diff --git a/app/assets/javascripts/clusters_list/store/index.js b/app/assets/javascripts/clusters_list/store/index.js
index 47e17b3624b..7cdd93eeae9 100644
--- a/app/assets/javascripts/clusters_list/store/index.js
+++ b/app/assets/javascripts/clusters_list/store/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import state from './state';
-import mutations from './mutations';
import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/code_navigation/components/app.vue b/app/assets/javascripts/code_navigation/components/app.vue
index 85ec0a60ec5..d38b38947b6 100644
--- a/app/assets/javascripts/code_navigation/components/app.vue
+++ b/app/assets/javascripts/code_navigation/components/app.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapState } from 'vuex';
-import Popover from './popover.vue';
import eventHub from '../../notes/event_hub';
+import Popover from './popover.vue';
export default {
components: {
diff --git a/app/assets/javascripts/code_navigation/index.js b/app/assets/javascripts/code_navigation/index.js
index 38b3467dc33..83906245b81 100644
--- a/app/assets/javascripts/code_navigation/index.js
+++ b/app/assets/javascripts/code_navigation/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import createStore from './store';
import App from './components/app.vue';
+import createStore from './store';
export default (initialData) => {
const el = document.getElementById('js-code-navigation');
diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js
index fb77a70de0b..0b6b8437db5 100644
--- a/app/assets/javascripts/code_navigation/store/actions.js
+++ b/app/assets/javascripts/code_navigation/store/actions.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
-import * as types from './mutation_types';
import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
+import * as types from './mutation_types';
export default {
setInitialData({ commit }, data) {
diff --git a/app/assets/javascripts/code_navigation/store/index.js b/app/assets/javascripts/code_navigation/store/index.js
index 9b60fc337fe..6b448bc5fb7 100644
--- a/app/assets/javascripts/code_navigation/store/index.js
+++ b/app/assets/javascripts/code_navigation/store/index.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
-import createState from './state';
import actions from './actions';
import mutations from './mutations';
+import createState from './state';
export default () =>
new Vuex.Store({
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 24033634aad..920ffde3e32 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import commitPipelinesTable from './pipelines_table.vue';
+import CommitPipelinesTable from './pipelines_table.vue';
/**
* Used in:
@@ -8,14 +8,6 @@ import commitPipelinesTable from './pipelines_table.vue';
* - Merge Request details View > Pipelines Tab > Pipelines Table (projects:merge_requests:show)
* - New Merge Request View > Pipelines Tab > Pipelines Table (projects:merge_requests:creations:new)
*/
-
-const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
-
-// export for use in merge_request_tabs.js (TODO: remove this hack when we understand how to load
-// vue.js in merge_request_tabs.js)
-window.gl = window.gl || {};
-window.gl.CommitPipelinesTable = CommitPipelinesTable;
-
export default () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
@@ -34,13 +26,17 @@ export default () => {
});
if (pipelineTableViewEl.dataset.disableInitialization === undefined) {
- const table = new CommitPipelinesTable({
- propsData: {
- endpoint: pipelineTableViewEl.dataset.endpoint,
- helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
- emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
- errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
- autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
+ const table = new Vue({
+ render(createElement) {
+ return createElement(CommitPipelinesTable, {
+ props: {
+ endpoint: pipelineTableViewEl.dataset.endpoint,
+ helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
+ emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
+ errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
+ autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
+ },
+ });
},
}).$mount();
pipelineTableViewEl.appendChild(table.$el);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index fe32868e6d8..787152d00ef 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -1,22 +1,25 @@
<script>
import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import SvgBlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
+import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
+import eventHub from '~/pipelines/event_hub';
+import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
import PipelinesService from '~/pipelines/services/pipelines_service';
import PipelineStore from '~/pipelines/stores/pipelines_store';
-import pipelinesMixin from '~/pipelines/mixins/pipelines';
-import eventHub from '~/pipelines/event_hub';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-import { getParameterByName } from '~/lib/utils/common_utils';
-import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
export default {
components: {
- TablePagination,
GlButton,
+ GlLink,
GlLoadingIcon,
GlModal,
- GlLink,
+ PipelinesTableComponent,
+ TablePagination,
+ SvgBlankState,
},
- mixins: [pipelinesMixin, CIPaginationMixin],
+ mixins: [PipelinesMixin],
props: {
endpoint: {
type: String,
@@ -197,7 +200,7 @@ export default {
<gl-button
v-if="canRenderPipelineButton"
block
- class="gl-mt-3 gl-mb-0 gl-display-md-none"
+ class="gl-mt-3 gl-mb-0 gl-md-display-none"
variant="success"
data-testid="run_pipeline_button_mobile"
:loading="state.isRunningMergeRequestPipeline"
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index f82bea134a3..5cbe9a24fc4 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { n__ } from '~/locale';
+import axios from './lib/utils/axios_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import Pager from './pager';
-import axios from './lib/utils/axios_utils';
export default class CommitsList {
constructor(limit = 0) {
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 82384434e8f..314e4e911ee 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -1,12 +1,12 @@
/* eslint-disable func-names */
import $ from 'jquery';
-import { __ } from './locale';
-import axios from './lib/utils/axios_utils';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { fixTitle } from '~/tooltips';
import { deprecatedCreateFlash as flash } from './flash';
+import axios from './lib/utils/axios_utils';
import { capitalizeFirstCharacter } from './lib/utils/text_utility';
-import { fixTitle } from '~/tooltips';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __ } from './locale';
export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function () {
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index 58fe022b794..2f39bbacc7d 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -1,8 +1,8 @@
<script>
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
-import { __ } from '../../locale';
-import { deprecatedCreateFlash as createFlash } from '../../flash';
import Api from '../../api';
+import { deprecatedCreateFlash as createFlash } from '../../flash';
+import { __ } from '../../locale';
import state from '../state';
import Dropdown from './dropdown.vue';
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index f7d24c70864..08cf0197993 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -1,7 +1,7 @@
+import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
-import { debounce } from 'lodash';
import Cookies from 'js-cookie';
-import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
+import { debounce } from 'lodash';
import { parseBoolean } from '~/lib/utils/common_utils';
export const SIDEBAR_COLLAPSED_CLASS = 'js-sidebar-collapsed';
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
index 86580aa170b..7426e570864 100644
--- a/app/assets/javascripts/contributors/components/contributors.vue
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -1,13 +1,13 @@
<script>
-import { debounce, uniq } from 'lodash';
-import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
-import { __ } from '~/locale';
-import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import { debounce, uniq } from 'lodash';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { getDatesInRange } from '~/lib/utils/datetime_utility';
-import { xAxisLabelFormatter, dateFormatter } from '../utils';
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import { __ } from '~/locale';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
+import { xAxisLabelFormatter, dateFormatter } from '../utils';
export default {
components: {
diff --git a/app/assets/javascripts/contributors/stores/index.js b/app/assets/javascripts/contributors/stores/index.js
index bc739851aa7..38259f46d4c 100644
--- a/app/assets/javascripts/contributors/stores/index.js
+++ b/app/assets/javascripts/contributors/stores/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import state from './state';
-import mutations from './mutations';
-import * as getters from './getters';
import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue b/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue
index 7f4c3635119..fa0a17f3643 100644
--- a/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue
@@ -1,10 +1,10 @@
<script>
-import { isNil } from 'lodash';
-import $ from 'jquery';
import { GlIcon } from '@gitlab/ui';
-import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
-import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import $ from 'jquery';
+import { isNil } from 'lodash';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
const toArray = (value) => (isNil(value) ? [] : [].concat(value));
const itemsProp = (items, prop) => items.map((item) => item[prop]);
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
index eb195ad2b30..ba170dc0e19 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
@@ -1,7 +1,7 @@
<script>
import { mapState } from 'vuex';
-import ServiceCredentialsForm from './service_credentials_form.vue';
import EksClusterConfigurationForm from './eks_cluster_configuration_form.vue';
+import ServiceCredentialsForm from './service_credentials_form.vue';
export default {
components: {
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index a7425735733..4aee02e45c8 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -1,5 +1,4 @@
<script>
-import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex';
import {
GlFormGroup,
GlFormInput,
@@ -9,8 +8,9 @@ import {
GlSprintf,
GlButton,
} from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
+import { s__ } from '~/locale';
import { KUBERNETES_VERSIONS } from '../constants';
const { mapState: mapRolesState, mapActions: mapRolesActions } = createNamespacedHelpers('roles');
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
index a3f76241bf2..f104eb61e41 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
@@ -1,11 +1,11 @@
<script>
/* eslint-disable vue/no-v-html */
-import { GlButton, GlFormGroup, GlFormInput, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormInput, GlIcon, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapState, mapActions } from 'vuex';
-import { DEFAULT_REGION } from '../constants';
import { sprintf, s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { DEFAULT_REGION } from '../constants';
export default {
components: {
@@ -16,6 +16,7 @@ export default {
GlLink,
GlSprintf,
ClipboardButton,
+ GlAlert,
},
props: {
accountAndExternalIdsHelpPath: {
@@ -105,9 +106,14 @@ export default {
)
}}
</p>
- <div v-if="createRoleError" class="js-invalid-credentials bs-callout bs-callout-danger">
+ <gl-alert
+ v-if="createRoleError"
+ class="js-invalid-credentials gl-mb-5"
+ variant="danger"
+ :dismissible="false"
+ >
{{ createRoleError }}
- </div>
+ </gl-alert>
<div class="form-row">
<div class="form-group col-md-6">
<label for="gitlab-account-id">{{ __('Account ID') }}</label>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
index c2b59191997..bd9554521b8 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
@@ -1,6 +1,6 @@
-import AWS from 'aws-sdk/global';
import EC2 from 'aws-sdk/clients/ec2';
import IAM from 'aws-sdk/clients/iam';
+import AWS from 'aws-sdk/global';
const lookupVpcName = ({ Tags: tags, VpcId: id }) => {
const nameTag = tags.find(({ Key: key }) => key === 'Name');
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
index 55576efd3b8..8b7c93ad880 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
@@ -1,9 +1,9 @@
-import * as types from './mutation_types';
-import { DEFAULT_REGION } from '../constants';
-import { setAWSConfig } from '../services/aws_services_facade';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { DEFAULT_REGION } from '../constants';
+import { setAWSConfig } from '../services/aws_services_facade';
+import * as types from './mutation_types';
const getErrorMessage = (data) => {
const errorKey = Object.keys(data)[0];
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
index 262bbb3167a..ed054989771 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
@@ -1,11 +1,5 @@
import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
import clusterDropdownStore from '~/create_cluster/store/cluster_dropdown';
-
import {
fetchRoles,
fetchKeyPairs,
@@ -13,6 +7,10 @@ import {
fetchSubnets,
fetchSecurityGroups,
} from '../services/aws_services_facade';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
const createStore = ({ initialState }) =>
new Vuex.Store({
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js
index f9d0d86e381..1246fdb19d7 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js
@@ -1,7 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
-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 DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import store from '../store';
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
index acbc4d1b3bc..b6f0bdbf01d 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
+import { mapState, mapGetters, mapActions } from 'vuex';
import { s__ } from '~/locale';
import gkeDropdownMixin from './gke_dropdown_mixin';
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/index.js b/app/assets/javascripts/create_cluster/gke_cluster/index.js
index c02173fc510..4eafbdb7265 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/index.js
+++ b/app/assets/javascripts/create_cluster/gke_cluster/index.js
@@ -1,15 +1,14 @@
import Vue from 'vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
-import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
-import GkeZoneDropdown from './components/gke_zone_dropdown.vue';
import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue';
+import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
import GkeSubmitButton from './components/gke_submit_button.vue';
+import GkeZoneDropdown from './components/gke_zone_dropdown.vue';
+import * as CONSTANTS from './constants';
import gapiLoader from './gapi_loader';
import store from './store';
-import * as CONSTANTS from './constants';
-
const mountComponent = (entryPoint, component, componentName, extraProps = {}) => {
const el = document.querySelector(entryPoint);
if (!el) return false;
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js b/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
index 8977053297a..f4c35dafc22 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import gapiLoader from '../gapi_loader';
+import * as types from './mutation_types';
const gapiResourceListRequest = ({ resource, params, commit, mutation, payloadKey }) =>
new Promise((resolve, reject) => {
diff --git a/app/assets/javascripts/create_cluster/init_create_cluster.js b/app/assets/javascripts/create_cluster/init_create_cluster.js
index f97da3d55db..d367d7ec333 100644
--- a/app/assets/javascripts/create_cluster/init_create_cluster.js
+++ b/app/assets/javascripts/create_cluster/init_create_cluster.js
@@ -1,6 +1,6 @@
+import PersistentUserCallout from '~/persistent_user_callout';
import initGkeDropdowns from './gke_cluster';
import initGkeNamespace from './gke_cluster_namespace';
-import PersistentUserCallout from '~/persistent_user_callout';
const newClusterViews = [':clusters:new', ':clusters:create_gcp', ':clusters:create_user'];
diff --git a/app/assets/javascripts/create_cluster/store/cluster_dropdown/index.js b/app/assets/javascripts/create_cluster/store/cluster_dropdown/index.js
index 0b19589215c..de8cc44fa7c 100644
--- a/app/assets/javascripts/create_cluster/store/cluster_dropdown/index.js
+++ b/app/assets/javascripts/create_cluster/store/cluster_dropdown/index.js
@@ -1,5 +1,5 @@
-import * as getters from './getters';
import actions from './actions';
+import * as getters from './getters';
import mutations from './mutations';
import state from './state';
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index aaaa7055799..35176c19f69 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -1,16 +1,16 @@
/* eslint-disable no-new */
import { debounce } from 'lodash';
-import axios from './lib/utils/axios_utils';
-import { deprecatedCreateFlash as Flash } from './flash';
-import DropLab from './droplab/drop_lab';
-import ISetter from './droplab/plugins/input_setter';
-import { __, sprintf } from './locale';
import {
init as initConfidentialMergeRequest,
isConfidentialIssue,
canCreateConfidentialMergeRequest,
} from './confidential_merge_request';
import confidentialMergeRequestState from './confidential_merge_request/state';
+import DropLab from './droplab/drop_lab';
+import ISetter from './droplab/plugins/input_setter';
+import { deprecatedCreateFlash as Flash } from './flash';
+import axios from './lib/utils/axios_utils';
+import { __, sprintf } from './locale';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = { ...ISetter };
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
index 9c28801306c..3158ae9b126 100644
--- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
@@ -1,10 +1,10 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
import csrf from '~/lib/utils/csrf';
+import { __, s__ } from '~/locale';
+import { formDataValidator } from '../constants';
import CustomMetricsFormFields from './custom_metrics_form_fields.vue';
import DeleteCustomMetricModal from './delete_custom_metric_modal.vue';
-import { formDataValidator } from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
index f3fa28dc2f3..0aae63e1648 100644
--- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
@@ -8,11 +8,11 @@ import {
GlIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import { __, s__ } from '~/locale';
-import csrf from '~/lib/utils/csrf';
import axios from '~/lib/utils/axios_utils';
-import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
+import csrf from '~/lib/utils/csrf';
+import statusCodes from '~/lib/utils/http_status';
+import { __, s__ } from '~/locale';
import { queryTypes, formDataValidator } from '../constants';
const VALIDATION_REQUEST_TIMEOUT = 10000;
diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue
index 4448d909c9b..cf4c35ef12b 100644
--- a/app/assets/javascripts/cycle_analytics/components/banner.vue
+++ b/app/assets/javascripts/cycle_analytics/components/banner.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
-import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
import { GlIcon } from '@gitlab/ui';
+import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
export default {
components: {
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index bd5a6cc40c4..847820c965f 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -1,17 +1,21 @@
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it
+// relies on app/views/projects/cycle_analytics/show.html.haml for its
+// template.
+/* eslint-disable @gitlab/no-runtime-template-compiler */
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import $ from 'jquery';
-import Vue from 'vue';
import Cookies from 'js-cookie';
-import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
-import { deprecatedCreateFlash as Flash } from '../flash';
+import Vue from 'vue';
import { __ } from '~/locale';
+import { deprecatedCreateFlash as Flash } from '../flash';
import Translate from '../vue_shared/translate';
import banner from './components/banner.vue';
import stageCodeComponent from './components/stage_code_component.vue';
import stageComponent from './components/stage_component.vue';
+import stageNavItem from './components/stage_nav_item.vue';
import stageReviewComponent from './components/stage_review_component.vue';
import stageStagingComponent from './components/stage_staging_component.vue';
import stageTestComponent from './components/stage_test_component.vue';
-import stageNavItem from './components/stage_nav_item.vue';
import CycleAnalyticsService from './cycle_analytics_service';
import CycleAnalyticsStore from './cycle_analytics_store';
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 7b04494892e..24ad6ef4c88 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
-import { __ } from '../locale';
import { dasherize } from '../lib/utils/text_utility';
+import { __ } from '../locale';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = {
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
index afc1c2cda8e..d05a0761ae3 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
@@ -1,10 +1,10 @@
<script>
import { GlFormGroup, GlFormInput, GlModal, GlSprintf, GlLink } from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
import { isValidCron } from 'cron-validator';
-import { mapComputed } from '~/vuex_shared/bindings';
+import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
+import { mapComputed } from '~/vuex_shared/bindings';
export default {
components: {
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue
index fc2ed10f3ca..0f898f20033 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue
@@ -1,6 +1,6 @@
<script>
-import DeployFreezeTable from './deploy_freeze_table.vue';
import DeployFreezeModal from './deploy_freeze_modal.vue';
+import DeployFreezeTable from './deploy_freeze_table.vue';
export default {
components: {
diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js
index 9a75c3cad2f..62045d2517d 100644
--- a/app/assets/javascripts/deploy_freeze/store/actions.js
+++ b/app/assets/javascripts/deploy_freeze/store/actions.js
@@ -1,7 +1,7 @@
-import * as types from './mutation_types';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
+import * as types from './mutation_types';
export const requestAddFreezePeriod = ({ commit }) => {
commit(types.REQUEST_ADD_FREEZE_PERIOD);
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 92e80d15902..425cca13ae8 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import { s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 3ddaba7abcc..e70ca18bb71 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,6 +1,6 @@
<script>
-import { head, tail } from 'lodash';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { head, tail } from 'lodash';
import { s__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -97,7 +97,7 @@ export default {
methods: {
projectTooltipTitle(project) {
return project.can_push
- ? s__('DeployKeys|Write access allowed')
+ ? s__('DeployKeys|Grant write permissions to this key')
: s__('DeployKeys|Read access only');
},
toggleExpanded() {
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
index 2693cd08cc3..d71f4f5507f 100644
--- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -48,7 +48,7 @@ export default {
:project-id="projectId"
/>
</template>
- <div v-else class="settings-message text-center">
+ <div v-else class="settings-message text-center gl-mt-5">
{{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div>
</div>
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
index 99351231520..162491312a8 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
@@ -1,13 +1,13 @@
/* eslint-disable consistent-return */
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
import $ from 'jquery';
import { escape } from 'lodash';
-import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { visitUrl } from '~/lib/utils/url_utility';
import { isObject } from '~/lib/utils/type_utility';
-import renderItem from './render';
-import { GitLabDropdownRemote } from './gl_dropdown_remote';
-import { GitLabDropdownInput } from './gl_dropdown_input';
+import { visitUrl } from '~/lib/utils/url_utility';
import { GitLabDropdownFilter } from './gl_dropdown_filter';
+import { GitLabDropdownInput } from './gl_dropdown_input';
+import { GitLabDropdownRemote } from './gl_dropdown_remote';
+import renderItem from './render';
const LOADING_CLASS = 'is-loading';
@@ -437,6 +437,7 @@ export class GitLabDropdown {
groupName = el.data('group');
if (groupName) {
selectedIndex = el.data('index');
+ this.selectedIndex = selectedIndex;
selectedObject = this.renderedData[groupName][selectedIndex];
} else {
selectedIndex = el.closest('li').index();
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
index ab9fb1ec332..b1d486c5d66 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
@@ -1,7 +1,7 @@
/* eslint-disable consistent-return */
-import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import $ from 'jquery';
import { isObject } from '~/lib/utils/type_utility';
const BLUR_KEYCODES = [27, 40];
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index ea4d5d7b570..33f0aa00cad 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,20 +1,20 @@
<script>
-import { ApolloMutation } from 'vue-apollo';
import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink, GlBadge } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { ApolloMutation } from 'vue-apollo';
import createFlash from '~/flash';
+import { s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import allVersionsMixin from '../../mixins/all_versions';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
+import allVersionsMixin from '../../mixins/all_versions';
+import { hasErrors } from '../../utils/cache_update';
+import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
import DesignNote from './design_note.vue';
import DesignReplyForm from './design_reply_form.vue';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
-import { hasErrors } from '../../utils/cache_update';
-import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
export default {
components: {
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index 421a4dc274a..2b867217327 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -1,13 +1,13 @@
<script>
-import { ApolloMutation } from 'vue-apollo';
import { GlTooltipDirective, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
-import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { ApolloMutation } from 'vue-apollo';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import DesignReplyForm from './design_reply_form.vue';
-import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
import { hasErrors } from '../../utils/cache_update';
+import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
+import DesignReplyForm from './design_reply_form.vue';
export default {
components: {
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index 0cc89440754..fb25d3618ab 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton, GlModal } from '@gitlab/ui';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
export default {
name: 'DesignReplyForm',
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 3c2ce693bc0..ecca8606f89 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -1,9 +1,9 @@
<script>
import { __ } from '~/locale';
-import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
+import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
import DesignNotePin from './design_note_pin.vue';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
export default {
name: 'DesignOverlay',
diff --git a/app/assets/javascripts/design_management/components/design_presentation.vue b/app/assets/javascripts/design_management/components/design_presentation.vue
index a760adf8b14..11d2f3b2e37 100644
--- a/app/assets/javascripts/design_management/components/design_presentation.vue
+++ b/app/assets/javascripts/design_management/components/design_presentation.vue
@@ -1,7 +1,7 @@
<script>
import { throttle } from 'lodash';
-import DesignImage from './image.vue';
import DesignOverlay from './design_overlay.vue';
+import DesignImage from './image.vue';
const CLICK_DRAG_BUFFER_PX = 2;
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index 50b12fd739b..efa1ef2107a 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -1,15 +1,15 @@
<script>
-import Cookies from 'js-cookie';
import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
+import Participants from '~/sidebar/components/participants/participants.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import { extractDiscussions, extractParticipants } from '../utils/design_management_utils';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import DesignDiscussion from './design_notes/design_discussion.vue';
-import Participants from '~/sidebar/components/participants/participants.vue';
import DesignTodoButton from './design_todo_button.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
diff --git a/app/assets/javascripts/design_management/components/design_todo_button.vue b/app/assets/javascripts/design_management/components/design_todo_button.vue
index db14db79989..da492f03801 100644
--- a/app/assets/javascripts/design_management/components/design_todo_button.vue
+++ b/app/assets/javascripts/design_management/components/design_todo_button.vue
@@ -1,8 +1,8 @@
<script>
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
-import getDesignQuery from '../graphql/queries/get_design.query.graphql';
-import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
import TodoButton from '~/vue_shared/components/todo_button.vue';
+import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
+import getDesignQuery from '../graphql/queries/get_design.query.graphql';
import allVersionsMixin from '../mixins/all_versions';
import { updateStoreAfterDeleteDesignTodo } from '../utils/cache_update';
import { findIssueId, findDesignId } from '../utils/design_management_utils';
diff --git a/app/assets/javascripts/design_management/components/image.vue b/app/assets/javascripts/design_management/components/image.vue
index 53062e6ebb0..e64ee4a5a34 100644
--- a/app/assets/javascripts/design_management/components/image.vue
+++ b/app/assets/javascripts/design_management/components/image.vue
@@ -1,6 +1,6 @@
<script>
-import { throttle } from 'lodash';
import { GlIcon } from '@gitlab/ui';
+import { throttle } from 'lodash';
export default {
components: {
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index fa09c7c15cc..5eabe9ef1bc 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -1,7 +1,7 @@
<script>
-import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
-import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
+import { GlLoadingIcon, GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import { n__, __ } from '~/locale';
+import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { DESIGN_ROUTE_NAME } from '../../router/constants';
export default {
@@ -11,6 +11,9 @@ export default {
GlIcon,
Timeago,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
id: {
type: [Number, String],
@@ -130,7 +133,7 @@ export default {
<div
class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
>
- <div v-if="icon.name" data-testid="designEvent" class="design-event gl-absolute">
+ <div v-if="icon.name" data-testid="design-event" class="gl-top-5 gl-right-5 gl-absolute">
<span :title="icon.tooltip" :aria-label="icon.tooltip">
<gl-icon
:name="icon.name"
@@ -153,9 +156,10 @@ export default {
v-show="showImage"
:src="imageLink"
:alt="filename"
- class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full design-img"
data-qa-selector="design_image"
:data-qa-filename="filename"
+ :data-testid="`design-img-${id}`"
@load="onImageLoad"
@error="onImageError"
/>
@@ -163,9 +167,14 @@ export default {
</div>
<div class="card-footer gl-display-flex gl-w-full">
<div class="gl-display-flex gl-flex-direction-column str-truncated-100">
- <span class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name">{{
- filename
- }}</span>
+ <span
+ v-gl-tooltip
+ class="gl-font-weight-bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ :data-testid="`design-img-filename-${id}`"
+ :title="filename"
+ >{{ filename }}</span
+ >
<span v-if="updatedAt" class="str-truncated-100">
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
</span>
diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue
index 3509a701984..a3b0f06fb28 100644
--- a/app/assets/javascripts/design_management/components/toolbar/index.vue
+++ b/app/assets/javascripts/design_management/components/toolbar/index.vue
@@ -3,9 +3,9 @@ import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import DesignNavigation from './design_navigation.vue';
-import DeleteButton from '../delete_button.vue';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
+import DeleteButton from '../delete_button.vue';
+import DesignNavigation from './design_navigation.vue';
export default {
components: {
diff --git a/app/assets/javascripts/design_management/graphql.js b/app/assets/javascripts/design_management/graphql.js
index b7aba315168..c6c3e66a01f 100644
--- a/app/assets/javascripts/design_management/graphql.js
+++ b/app/assets/javascripts/design_management/graphql.js
@@ -1,16 +1,16 @@
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import produce from 'immer';
+import { uniqueId } from 'lodash';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { uniqueId } from 'lodash';
-import produce from 'immer';
-import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
-import axios from '~/lib/utils/axios_utils';
import createDefaultClient from '~/lib/graphql';
+import axios from '~/lib/utils/axios_utils';
import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
import getDesignQuery from './graphql/queries/get_design.query.graphql';
import typeDefs from './graphql/typedefs.graphql';
+import { addPendingTodoToStore } from './utils/cache_update';
import { extractTodoIdFromDeletePath, createPendingTodo } from './utils/design_management_utils';
import { CREATE_DESIGN_TODO_EXISTS_ERROR } from './utils/error_messages';
-import { addPendingTodoToStore } from './utils/cache_update';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js
index 1a87dd38137..f0930ade1b5 100644
--- a/app/assets/javascripts/design_management/index.js
+++ b/app/assets/javascripts/design_management/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
-import createRouter from './router';
import App from './components/app.vue';
import apolloProvider from './graphql';
+import createRouter from './router';
export default () => {
const el = document.querySelector('.js-design-management');
diff --git a/app/assets/javascripts/design_management/mixins/all_designs.js b/app/assets/javascripts/design_management/mixins/all_designs.js
index 4783382d563..e92f8006a0d 100644
--- a/app/assets/javascripts/design_management/mixins/all_designs.js
+++ b/app/assets/javascripts/design_management/mixins/all_designs.js
@@ -2,8 +2,8 @@ import { propertyOf } from 'lodash';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import { s__ } from '~/locale';
-import allVersionsMixin from './all_versions';
import { DESIGNS_ROUTE_NAME } from '../router/constants';
+import allVersionsMixin from './all_versions';
export default {
mixins: [allVersionsMixin],
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 492ed2e8719..8a11c25a795 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -1,21 +1,27 @@
<script>
-import Mousetrap from 'mousetrap';
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import Mousetrap from 'mousetrap';
import { ApolloMutation } from 'vue-apollo';
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import allVersionsMixin from '../../mixins/all_versions';
-import Toolbar from '../../components/toolbar/index.vue';
import DesignDestroyer from '../../components/design_destroyer.vue';
-import DesignScaler from '../../components/design_scaler.vue';
-import DesignPresentation from '../../components/design_presentation.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
+import DesignPresentation from '../../components/design_presentation.vue';
+import DesignScaler from '../../components/design_scaler.vue';
import DesignSidebar from '../../components/design_sidebar.vue';
-import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
+import Toolbar from '../../components/toolbar/index.vue';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES, DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../../constants';
import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
import repositionImageDiffNoteMutation from '../../graphql/mutations/reposition_image_diff_note.mutation.graphql';
import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
+import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
+import allVersionsMixin from '../../mixins/all_versions';
+import { DESIGNS_ROUTE_NAME } from '../../router/constants';
+import {
+ updateStoreAfterAddImageDiffNote,
+ updateStoreAfterRepositionImageDiffNote,
+} from '../../utils/cache_update';
import {
extractDiscussions,
extractDesign,
@@ -25,10 +31,6 @@ import {
getPageLayoutElement,
} from '../../utils/design_management_utils';
import {
- updateStoreAfterAddImageDiffNote,
- updateStoreAfterRepositionImageDiffNote,
-} from '../../utils/cache_update';
-import {
ADD_DISCUSSION_COMMENT_ERROR,
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
@@ -39,8 +41,6 @@ import {
designDeletionError,
} from '../../utils/error_messages';
import { trackDesignDetailView, usagePingDesignDetailView } from '../../utils/tracking';
-import { DESIGNS_ROUTE_NAME } from '../../router/constants';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES, DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../../constants';
const DEFAULT_SCALE = 1;
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 5c82a7331b6..c73c8fb6ca4 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -1,29 +1,22 @@
<script>
import { GlLoadingIcon, GlButton, GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import VueDraggable from 'vuedraggable';
-import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
+import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
-import { __, s__, sprintf } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload';
-import UploadButton from '../components/upload/button.vue';
+import { __, s__, sprintf } from '~/locale';
+import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import DeleteButton from '../components/delete_button.vue';
-import Design from '../components/list/item.vue';
import DesignDestroyer from '../components/design_destroyer.vue';
+import Design from '../components/list/item.vue';
+import UploadButton from '../components/upload/button.vue';
import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue';
-import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
-import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
+import { VALID_DESIGN_FILE_MIMETYPE } from '../constants';
import moveDesignMutation from '../graphql/mutations/move_design.mutation.graphql';
+import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
import allDesignsMixin from '../mixins/all_designs';
-import {
- UPLOAD_DESIGN_ERROR,
- EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
- EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
- MOVE_DESIGN_ERROR,
- UPLOAD_DESIGN_INVALID_FILETYPE_ERROR,
- designUploadSkippedWarning,
- designDeletionError,
-} from '../utils/error_messages';
+import { DESIGNS_ROUTE_NAME } from '../router/constants';
import {
updateStoreAfterUploadDesign,
updateDesignsOnStoreAfterReorder,
@@ -33,9 +26,16 @@ import {
isValidDesignFile,
moveDesignOptimisticResponse,
} from '../utils/design_management_utils';
+import {
+ UPLOAD_DESIGN_ERROR,
+ EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
+ EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
+ MOVE_DESIGN_ERROR,
+ UPLOAD_DESIGN_INVALID_FILETYPE_ERROR,
+ designUploadSkippedWarning,
+ designDeletionError,
+} from '../utils/error_messages';
import { trackDesignCreate, trackDesignUpdate } from '../utils/tracking';
-import { DESIGNS_ROUTE_NAME } from '../router/constants';
-import { VALID_DESIGN_FILE_MIMETYPE } from '../constants';
const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
@@ -347,15 +347,20 @@ export default {
>
<header
v-if="showToolbar"
- class="row-content-block gl-border-t-0 gl-p-3 gl-display-flex"
+ class="row-content-block gl-border-t-0 gl-py-3 gl-display-flex"
data-testid="design-toolbar-wrapper"
>
- <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full">
- <div>
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full gl-flex-wrap"
+ >
+ <div class="gl-display-flex gl-align-items-center gl-my-2">
<span class="gl-font-weight-bold gl-mr-3">{{ s__('DesignManagement|Designs') }}</span>
<design-version-dropdown />
</div>
- <div v-show="hasDesigns" class="qa-selector-toolbar gl-display-flex gl-align-items-center">
+ <div
+ v-show="hasDesigns"
+ class="qa-selector-toolbar gl-display-flex gl-align-items-center gl-my-2"
+ >
<gl-button
v-if="isLatestVersion"
variant="link"
diff --git a/app/assets/javascripts/design_management/router/routes.js b/app/assets/javascripts/design_management/router/routes.js
index 1b07d8aeb76..fb1daecfc35 100644
--- a/app/assets/javascripts/design_management/router/routes.js
+++ b/app/assets/javascripts/design_management/router/routes.js
@@ -1,5 +1,5 @@
-import Home from '../pages/index.vue';
import DesignDetail from '../pages/design/index.vue';
+import Home from '../pages/index.vue';
import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
export default [
diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js
index 0c4ee0bf012..c561eda12ed 100644
--- a/app/assets/javascripts/design_management/utils/cache_update.js
+++ b/app/assets/javascripts/design_management/utils/cache_update.js
@@ -1,7 +1,7 @@
/* eslint-disable @gitlab/require-i18n-strings */
-import { differenceBy } from 'lodash';
import produce from 'immer';
+import { differenceBy } from 'lodash';
import createFlash from '~/flash';
import { extractCurrentDiscussion, extractDesign, extractDesigns } from './design_management_utils';
import {
diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js
index cb4bb6e26a8..e7b2c814bb3 100644
--- a/app/assets/javascripts/design_management/utils/error_messages.js
+++ b/app/assets/javascripts/design_management/utils/error_messages.js
@@ -44,13 +44,13 @@ export const MOVE_DESIGN_ERROR = __(
'Something went wrong when reordering designs. Please try again',
);
-export const CREATE_DESIGN_TODO_ERROR = __('Failed to create To-Do for the design.');
+export const CREATE_DESIGN_TODO_ERROR = __('Failed to create a to-do item for the design.');
-export const CREATE_DESIGN_TODO_EXISTS_ERROR = __('There is already a To-Do for this design.');
+export const CREATE_DESIGN_TODO_EXISTS_ERROR = __('There is already a to-do item for this design.');
-export const DELETE_DESIGN_TODO_ERROR = __('Failed to remove To-Do for the design.');
+export const DELETE_DESIGN_TODO_ERROR = __('Failed to remove a to-do item for the design.');
-export const TOGGLE_TODO_ERROR = __('Failed to toggle To-Do for the design.');
+export const TOGGLE_TODO_ERROR = __('Failed to toggle the to-do status for the design.');
const MAX_SKIPPED_FILES_LISTINGS = 5;
diff --git a/app/assets/javascripts/design_management/utils/tracking.js b/app/assets/javascripts/design_management/utils/tracking.js
index 37296f5b4ff..905134fa985 100644
--- a/app/assets/javascripts/design_management/utils/tracking.js
+++ b/app/assets/javascripts/design_management/utils/tracking.js
@@ -1,5 +1,5 @@
-import Tracking from '~/tracking';
import Api from '~/api';
+import Tracking from '~/tracking';
// Snowplow tracking constants
const DESIGN_TRACKING_CONTEXT_SCHEMAS = {
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index c4e86638e9d..7200e6c2e3a 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import { getLocationHash } from './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
-import SingleFileDiff from './single_file_diff';
import initImageDiffHelper from './image_diff/helpers/init_image_diff';
+import { getLocationHash } from './lib/utils/url_utility';
+import SingleFileDiff from './single_file_diff';
const UNFOLD_COUNT = 20;
let isBound = false;
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 32822fe1fe8..4323499ef1f 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -1,32 +1,17 @@
<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon, GlPagination, GlSprintf } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Mousetrap from 'mousetrap';
-import { __ } from '~/locale';
-import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
+import { mapState, mapGetters, mapActions } from 'vuex';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
+import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import notesEventHub from '../../notes/event_hub';
-import eventHub from '../event_hub';
-
-import CompareVersions from './compare_versions.vue';
-import DiffFile from './diff_file.vue';
-import NoChanges from './no_changes.vue';
-import CommitWidget from './commit_widget.vue';
-import TreeList from './tree_list.vue';
-
-import HiddenFilesWarning from './hidden_files_warning.vue';
-import MergeConflictWarning from './merge_conflict_warning.vue';
-import CollapsedFilesWarning from './collapsed_files_warning.vue';
-
-import { diffsApp } from '../utils/performance';
-import { fileByFile } from '../utils/preferences';
-
import {
TREE_LIST_WIDTH_STORAGE_KEY,
INITIAL_TREE_WIDTH,
@@ -40,6 +25,19 @@ import {
ALERT_COLLAPSED_FILES,
EVT_VIEW_FILE_BY_FILE,
} from '../constants';
+import eventHub from '../event_hub';
+
+import { reviewStatuses } from '../utils/file_reviews';
+import { diffsApp } from '../utils/performance';
+import { fileByFile } from '../utils/preferences';
+import CollapsedFilesWarning from './collapsed_files_warning.vue';
+import CommitWidget from './commit_widget.vue';
+import CompareVersions from './compare_versions.vue';
+import DiffFile from './diff_file.vue';
+import HiddenFilesWarning from './hidden_files_warning.vue';
+import MergeConflictWarning from './merge_conflict_warning.vue';
+import NoChanges from './no_changes.vue';
+import TreeList from './tree_list.vue';
export default {
name: 'DiffsApp',
@@ -169,12 +167,7 @@ export default {
'hasConflicts',
'viewDiffsFileByFile',
]),
- ...mapGetters('diffs', [
- 'whichCollapsedTypes',
- 'isParallelView',
- 'currentDiffIndex',
- 'fileReviews',
- ]),
+ ...mapGetters('diffs', ['whichCollapsedTypes', 'isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
if (!this.viewDiffsFileByFile) {
@@ -232,6 +225,9 @@ export default {
return visible;
},
+ fileReviews() {
+ return reviewStatuses(this.diffFiles, this.mrReviews);
+ },
},
watch: {
commit(newCommit, oldCommit) {
@@ -526,7 +522,7 @@ export default {
:file="file"
:reviewed="fileReviews[index]"
:is-first-file="index === 0"
- :is-last-file="index === diffs.length - 1"
+ :is-last-file="index === diffFilesLength - 1"
:help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index af19f90ee77..92b317eb3f0 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -1,18 +1,16 @@
<script>
/* eslint-disable vue/no-v-html */
-import { mapActions } from 'vuex';
import { GlButtonGroup, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { mapActions } from 'vuex';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
-
-import initUserPopovers from '../../user_popovers';
import { setUrlParams } from '../../lib/utils/url_utility';
+import initUserPopovers from '../../user_popovers';
/**
* CommitItem
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 489278fd6ef..6b1e2bfb34e 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -1,13 +1,12 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlLink, GlButton, GlSprintf } from '@gitlab/ui';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { __ } from '~/locale';
-import { polyfillSticky } from '~/lib/utils/sticky';
-import CompareDropdownLayout from './compare_dropdown_layout.vue';
-import SettingsDropdown from './settings_dropdown.vue';
-import DiffStats from './diff_stats.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '../constants';
import eventHub from '../event_hub';
+import CompareDropdownLayout from './compare_dropdown_layout.vue';
+import DiffStats from './diff_stats.vue';
+import SettingsDropdown from './settings_dropdown.vue';
export default {
components: {
@@ -61,9 +60,6 @@ export default {
created() {
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
},
- mounted() {
- polyfillSticky(this.$el);
- },
methods: {
...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'setShowTreeList']),
expandAllFiles() {
diff --git a/app/assets/javascripts/diffs/components/diff_comment_cell.vue b/app/assets/javascripts/diffs/components/diff_comment_cell.vue
index 4b0b603f5a5..4af4b46f94c 100644
--- a/app/assets/javascripts/diffs/components/diff_comment_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_comment_cell.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions } from 'vuex';
+import DiffDiscussionReply from './diff_discussion_reply.vue';
import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
-import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index f4e2571dd09..663d2bb3cf8 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -1,25 +1,25 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
+import { mapActions, mapGetters, mapState } from 'vuex';
+import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
+import { diffViewerModes } from '~/ide/constants';
+import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
-import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
-import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
-import InlineDiffView from './inline_diff_view.vue';
-import ParallelDiffView from './parallel_diff_view.vue';
-import DiffView from './diff_view.vue';
-import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import NoteForm from '../../notes/components/note_form.vue';
-import ImageDiffOverlay from './image_diff_overlay.vue';
-import DiffDiscussions from './diff_discussions.vue';
import eventHub from '../../notes/event_hub';
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
import { getDiffMode } from '../store/utils';
-import { diffViewerModes } from '~/ide/constants';
+import DiffDiscussions from './diff_discussions.vue';
import { mapInline, mapParallel } from './diff_row_utils';
+import DiffView from './diff_view.vue';
+import ImageDiffOverlay from './image_diff_overlay.vue';
+import InlineDiffView from './inline_diff_view.vue';
+import ParallelDiffView from './parallel_diff_view.vue';
export default {
components: {
diff --git a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
index 531ebaddacd..9027d0c8aa4 100644
--- a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
@@ -1,15 +1,13 @@
<script>
import { mapGetters } from 'vuex';
-import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
export default {
name: 'DiffDiscussionReply',
components: {
NoteSignedOutWidget,
ReplyPlaceholder,
- UserAvatarLink,
},
props: {
hasForm: {
@@ -36,13 +34,6 @@ export default {
<template v-if="userCanReply">
<slot v-if="hasForm" name="form"></slot>
<template v-else-if="renderReplyPlaceholder">
- <user-avatar-link
- :link-href="currentUser.path"
- :img-src="currentUser.avatar_url"
- :img-alt="currentUser.name"
- :img-size="40"
- class="d-none d-sm-block"
- />
<reply-placeholder
:button-text="__('Start a new discussion...')"
@onClick="$emit('showNewDiscussionForm')"
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 7b55bd2104d..d0d457d8582 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions } from 'vuex';
import { GlIcon } from '@gitlab/ui';
+import { mapActions } from 'vuex';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default {
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 2d1a7237122..67900af8789 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlIcon } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index e613b684345..f77c8d7406b 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,16 +1,14 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml, GlSprintf } from '@gitlab/ui';
import { escape } from 'lodash';
-import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { sprintf } from '~/locale';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
-import notesEventHub from '../../notes/event_hub';
-import DiffFileHeader from './diff_file_header.vue';
-import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants';
-import { collapsedType, isCollapsed } from '../utils/diff_file';
+import { sprintf } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import notesEventHub from '../../notes/event_hub';
+
import {
DIFF_FILE_AUTOMATIC_COLLAPSE,
DIFF_FILE_MANUAL_COLLAPSE,
@@ -18,8 +16,11 @@ import {
EVT_PERF_MARK_DIFF_FILES_END,
EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
} from '../constants';
-import { DIFF_FILE, GENERIC_ERROR } from '../i18n';
import eventHub from '../event_hub';
+import { DIFF_FILE, GENERIC_ERROR } from '../i18n';
+import { collapsedType, isCollapsed, getShortShaFromFile } from '../utils/diff_file';
+import DiffContent from './diff_content.vue';
+import DiffFileHeader from './diff_file_header.vue';
export default {
components: {
@@ -27,6 +28,7 @@ export default {
DiffContent,
GlButton,
GlLoadingIcon,
+ GlSprintf,
},
directives: {
SafeHtml,
@@ -81,15 +83,11 @@ export default {
...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched']),
...mapGetters('diffs', ['getDiffFileDiscussions']),
- viewBlobLink() {
- return sprintf(
- this.$options.i18n.blobView,
- {
- linkStart: `<a href="${escape(this.file.view_path)}">`,
- linkEnd: '</a>',
- },
- false,
- );
+ viewBlobHref() {
+ return escape(this.file.view_path);
+ },
+ shortSha() {
+ return getShortShaFromFile(this.file);
},
showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
@@ -98,7 +96,7 @@ export default {
return hasDiff(this.file);
},
isFileTooLarge() {
- return this.file.viewer.error === diffViewerErrors.too_large;
+ return !this.manuallyCollapsed && this.file.viewer.error === diffViewerErrors.too_large;
},
errorMessage() {
return !this.manuallyCollapsed ? this.file.viewer.error_message : '';
@@ -144,6 +142,12 @@ export default {
showContent() {
return !this.isCollapsed && !this.isFileTooLarge;
},
+ showLocalFileReviews() {
+ const loggedIn = Boolean(gon.current_user_id);
+ const featureOn = this.glFeatures.localFileReviews;
+
+ return loggedIn && featureOn;
+ },
},
watch: {
'file.file_hash': {
@@ -181,6 +185,10 @@ export default {
if (this.hasDiff) {
this.postRender();
}
+
+ if (this.reviewed && !this.isCollapsed && this.showLocalFileReviews) {
+ this.handleToggle();
+ }
},
beforeDestroy() {
eventHub.$off(EVT_EXPAND_ALL_FILES, this.expandAllListener);
@@ -273,9 +281,11 @@ export default {
:can-current-user-fork="canCurrentUserFork"
:diff-file="file"
:collapsible="true"
+ :reviewed="reviewed"
:expanded="!isCollapsed"
:add-merge-request-buttons="true"
:view-diffs-file-by-file="viewDiffsFileByFile"
+ :show-local-file-reviews="showLocalFileReviews"
class="js-file-title file-title gl-border-1 gl-border-solid gl-border-gray-100"
:class="hasBodyClasses.header"
@toggleFile="handleToggle"
@@ -309,14 +319,27 @@ export default {
data-testid="loader-icon"
/>
<div v-else-if="errorMessage" class="diff-viewer">
- <div v-safe-html="errorMessage" class="nothing-here-block"></div>
+ <div
+ v-if="isFileTooLarge"
+ class="collapsed-file-warning gl-p-7 gl-bg-orange-50 gl-text-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
+ >
+ <p class="gl-mb-5">
+ {{ $options.i18n.tooLarge }}
+ </p>
+ <gl-button data-testid="blob-button" category="secondary" :href="viewBlobHref">
+ <gl-sprintf :message="$options.i18n.blobView">
+ <template #commitSha>{{ shortSha }}</template>
+ </gl-sprintf>
+ </gl-button>
+ </div>
+ <div v-else v-safe-html="errorMessage" class="nothing-here-block"></div>
</div>
<template v-else>
<div
v-show="showWarning"
class="collapsed-file-warning gl-p-7 gl-bg-orange-50 gl-text-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
>
- <p class="gl-mb-8">
+ <p class="gl-mb-5">
{{ $options.i18n.autoCollapsed }}
</p>
<gl-button
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 53d1383b82e..1195a7f2565 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -1,6 +1,4 @@
<script>
-import { escape } from 'lodash';
-import { mapActions, mapGetters } from 'vuex';
import {
GlTooltipDirective,
GlSafeHtmlDirective,
@@ -10,17 +8,25 @@ import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
+ GlFormCheckbox,
GlLoadingIcon,
} from '@gitlab/ui';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import FileIcon from '~/vue_shared/components/file_icon.vue';
-import { truncateSha } from '~/lib/utils/text_utility';
-import { __, s__, sprintf } from '~/locale';
+import { escape } from 'lodash';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { diffViewerModes } from '~/ide/constants';
-import DiffStats from './diff_stats.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
-import { isCollapsed } from '../utils/diff_file';
+import { truncateSha } from '~/lib/utils/text_utility';
+import { __, s__, sprintf } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+import { DIFF_FILE_AUTOMATIC_COLLAPSE } from '../constants';
import { DIFF_FILE_HEADER } from '../i18n';
+import { collapsedType, isCollapsed } from '../utils/diff_file';
+import { reviewable } from '../utils/file_reviews';
+
+import DiffStats from './diff_stats.vue';
export default {
components: {
@@ -33,12 +39,14 @@ export default {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
+ GlFormCheckbox,
GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml: GlSafeHtmlDirective,
},
+ mixins: [glFeatureFlagsMixin()],
i18n: {
...DIFF_FILE_HEADER,
},
@@ -76,6 +84,16 @@ export default {
required: false,
default: false,
},
+ showLocalFileReviews: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ reviewed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -83,6 +101,7 @@ export default {
};
},
computed: {
+ ...mapState('diffs', ['latestDiff']),
...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
diffContentIDSelector() {
return `#diff-content-${this.diffFile.file_hash}`;
@@ -170,6 +189,9 @@ export default {
(this.diffFile.edit_path || this.diffFile.ide_edit_path)
);
},
+ isReviewable() {
+ return reviewable(this.diffFile);
+ },
},
methods: {
...mapActions('diffs', [
@@ -177,6 +199,8 @@ export default {
'toggleFileDiscussionWrappers',
'toggleFullDiff',
'toggleActiveFileByHash',
+ 'reviewFile',
+ 'setFileCollapsedByUser',
]),
handleToggleFile() {
this.$emit('toggleFile');
@@ -204,6 +228,26 @@ export default {
setMoreActionsShown(val) {
this.moreActionsShown = val;
},
+ toggleReview(newReviewedStatus) {
+ const autoCollapsed =
+ this.isCollapsed && collapsedType(this.diffFile) === DIFF_FILE_AUTOMATIC_COLLAPSE;
+ const open = this.expanded;
+ const closed = !open;
+ const reviewed = newReviewedStatus;
+
+ this.reviewFile({ file: this.diffFile, reviewed });
+
+ if (reviewed && autoCollapsed) {
+ this.setFileCollapsedByUser({
+ filePath: this.diffFile.file_path,
+ collapsed: true,
+ });
+ }
+
+ if ((open && reviewed) || (closed && !reviewed)) {
+ this.$emit('toggleFile');
+ }
+ },
},
};
</script>
@@ -213,6 +257,8 @@ export default {
ref="header"
:class="{ 'gl-z-dropdown-menu!': moreActionsShown }"
class="js-file-title file-title file-title-flex-parent"
+ data-qa-selector="file_title_container"
+ :data-qa-file-name="filePath"
@click.self="handleToggleFile"
>
<div class="file-header-content">
@@ -289,6 +335,19 @@ export default {
class="file-actions d-flex align-items-center gl-ml-auto gl-align-self-start"
>
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
+ <gl-form-checkbox
+ v-if="isReviewable && showLocalFileReviews"
+ v-gl-tooltip.hover
+ data-testid="fileReviewCheckbox"
+ class="gl-mb-0"
+ :title="$options.i18n.fileReviewTooltip"
+ :checked="reviewed"
+ @change="toggleReview"
+ >
+ <span class="gl-line-height-20">
+ {{ $options.i18n.fileReviewLabel }}
+ </span>
+ </gl-form-checkbox>
<gl-button-group class="gl-pt-0!">
<gl-button
v-if="diffFile.external_url"
@@ -307,6 +366,7 @@ export default {
right
toggle-class="btn-icon js-diff-more-actions"
class="gl-pt-0!"
+ data-qa-selector="dropdown_button"
@show="setMoreActionsShown(true)"
@hidden="setMoreActionsShown(false)"
>
@@ -340,6 +400,7 @@ export default {
ref="ideEditButton"
:href="diffFile.ide_edit_path"
class="js-ide-edit-blob"
+ data-qa-selector="edit_in_ide_button"
>
{{ __('Edit in Web IDE') }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/diffs/components/diff_file_row.vue b/app/assets/javascripts/diffs/components/diff_file_row.vue
index 6c5d9170c9e..89822ba7878 100644
--- a/app/assets/javascripts/diffs/components/diff_file_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_row.vue
@@ -3,9 +3,9 @@
* This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue`
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720
*/
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import FileRow from '~/vue_shared/components/file_row.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
+import FileRow from '~/vue_shared/components/file_row.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import FileRowStats from './file_row_stats.vue';
export default {
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index f62b31734c5..1f3ec7092bc 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { n__ } from '~/locale';
import { truncate } from '~/lib/utils/text_utility';
+import { n__ } from '~/locale';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index 463b7f5cff4..2f09f2e24b2 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -1,22 +1,20 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
+import { s__ } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { s__ } from '~/locale';
-import noteForm from '../../notes/components/note_form.vue';
import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue';
-import autosave from '../../notes/mixins/autosave';
-import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import { DIFF_NOTE_TYPE, INLINE_DIFF_LINES_KEY, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import {
commentLineOptions,
formatLineRange,
} from '../../notes/components/multiline_comment_utils';
+import noteForm from '../../notes/components/note_form.vue';
+import autosave from '../../notes/mixins/autosave';
+import { DIFF_NOTE_TYPE, INLINE_DIFF_LINES_KEY, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
export default {
components: {
noteForm,
- userAvatarLink,
MultilineCommentForm,
},
mixins: [autosave, diffLineNoteFormMixin, glFeatureFlagsMixin()],
@@ -167,21 +165,13 @@ export default {
<template>
<div class="content discussion-form discussion-form-container discussion-notes">
- <div v-if="glFeatures.multilineComments" class="gl-mb-3 gl-text-gray-500 gl-pb-3">
+ <div class="gl-mb-3 gl-text-gray-500 gl-pb-3">
<multiline-comment-form
v-model="commentLineStart"
:line="line"
:comment-line-options="commentLineOptions"
/>
</div>
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- class="d-none d-sm-block"
- />
<note-form
ref="noteForm"
:is-editing="true"
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index db03da966c3..ab6890d66b5 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -1,6 +1,8 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { mapActions, mapGetters, mapState } from 'vuex';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
CONTEXT_LINE_CLASS_NAME,
PARALLEL_DIFF_VIEW_TYPE,
@@ -10,7 +12,6 @@ import {
CONFLICT_THEIR,
CONFLICT_MARKER,
} from '../constants';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
@@ -99,10 +100,10 @@ export default {
});
},
addCommentTooltipLeft() {
- return utils.addCommentTooltip(this.line.left);
+ return utils.addCommentTooltip(this.line.left, this.glFeatures.dragCommentSelection);
},
addCommentTooltipRight() {
- return utils.addCommentTooltip(this.line.right);
+ return utils.addCommentTooltip(this.line.right, this.glFeatures.dragCommentSelection);
},
emptyCellRightClassMap() {
return { conflict_their: this.line.left?.type === CONFLICT_OUR };
@@ -111,13 +112,7 @@ export default {
return { conflict_our: this.line.right?.type === CONFLICT_THEIR };
},
shouldRenderCommentButton() {
- return (
- this.isLoggedIn &&
- !this.line.isContextLineLeft &&
- !this.line.isMetaLineLeft &&
- !this.line.hasDiscussionsLeft &&
- !this.line.hasDiscussionsRight
- );
+ return this.isLoggedIn && !this.line.isContextLineLeft && !this.line.isMetaLineLeft;
},
isLeftConflictMarker() {
return [CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR].includes(this.line.left?.type);
@@ -168,7 +163,7 @@ export default {
this.$emit('enterdragging', { ...line, index });
},
onDragStart(line) {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
this.dragging = true;
this.$emit('startdragging', line);
},
@@ -199,7 +194,7 @@ export default {
>
<template v-if="!isLeftConflictMarker">
<span
- v-if="shouldRenderCommentButton"
+ v-if="shouldRenderCommentButton && !line.hasDiscussionsLeft"
v-gl-tooltip
data-testid="leftCommentButton"
class="add-diff-note tooltip-wrapper"
@@ -301,7 +296,7 @@ export default {
<div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
<template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR">
<span
- v-if="shouldRenderCommentButton"
+ v-if="shouldRenderCommentButton && !line.hasDiscussionsRight"
v-gl-tooltip
data-testid="rightCommentButton"
class="add-diff-note tooltip-wrapper"
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index 7606c39ad37..cd45474afcd 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -50,11 +50,11 @@ export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => {
];
};
-export const addCommentTooltip = (line) => {
+export const addCommentTooltip = (line, dragCommentSelectionEnabled = false) => {
let tooltip;
if (!line) return tooltip;
- tooltip = gon.drag_comment_selection
+ tooltip = dragCommentSelectionEnabled
? __('Add a comment to this line or drag for multiple lines')
: __('Add a comment to this line');
const brokenSymlinks = line.commentsDisabled;
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
index f229fc4cf60..0303700f42a 100644
--- a/app/assets/javascripts/diffs/components/diff_stats.vue
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -1,6 +1,6 @@
<script>
-import { isNumber } from 'lodash';
import { GlIcon } from '@gitlab/ui';
+import { isNumber } from 'lodash';
import { n__ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 79800f835f4..43cfa22073f 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -1,11 +1,11 @@
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
-import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue';
-import DiffRow from './diff_row.vue';
+import draftCommentsMixin from '~/diffs/mixins/draft_comments';
+import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
-import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
+import DiffRow from './diff_row.vue';
export default {
components: {
@@ -61,10 +61,10 @@ export default {
...mapActions(['setSelectedCommentPosition']),
...mapActions('diffs', ['showCommentForm']),
showCommentLeft(line) {
- return !this.inline || line.left;
+ return line.left && !line.right;
},
showCommentRight(line) {
- return !this.inline || (line.right && !line.left);
+ return line.right && !line.left;
},
onStartDragging(line) {
this.dragStart = line;
@@ -138,24 +138,30 @@ export default {
:class="line.commentRowClasses"
class="diff-grid-comments diff-tr notes_holder"
>
- <div v-if="showCommentLeft(line)" class="diff-td notes-content parallel old">
+ <div
+ v-if="line.left || !inline"
+ :class="{ parallel: !inline }"
+ class="diff-td notes-content old"
+ >
<diff-comment-cell
- v-if="line.left"
+ v-if="line.left && (line.left.renderDiscussion || line.left.hasCommentForm)"
:line="line.left"
:diff-file-hash="diffFile.file_hash"
:help-page-path="helpPagePath"
- :has-draft="line.left.hasDraft"
line-position="left"
/>
</div>
- <div v-if="showCommentRight(line)" class="diff-td notes-content parallel new">
+ <div
+ v-if="line.right || !inline"
+ :class="{ parallel: !inline }"
+ class="diff-td notes-content new"
+ >
<diff-comment-cell
- v-if="line.right"
+ v-if="line.right && (line.right.renderDiscussion || line.right.hasCommentForm)"
:line="line.right"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
:help-page-path="helpPagePath"
- :has-draft="line.right.hasDraft"
line-position="right"
/>
</div>
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
index 6a1e0d8cbd6..3d05202fb2d 100644
--- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -1,8 +1,8 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { isArray } from 'lodash';
+import { mapActions, mapGetters } from 'vuex';
import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff';
-import { GlIcon } from '@gitlab/ui';
function calcPercent(pos, size, renderedSize) {
return (((pos / size) * 100) / ((renderedSize / size) * 100)) * 100;
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 014b1ebe54b..154f2fdcfc7 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { CONTEXT_LINE_CLASS_NAME } from '../constants';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import {
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index 28485a2fdac..e407609d9e9 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -1,12 +1,12 @@
<script>
import { mapGetters, mapState } from 'vuex';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue';
-import inlineDiffTableRow from './inline_diff_table_row.vue';
+import draftCommentsMixin from '~/diffs/mixins/draft_comments';
+import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
-import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
+import inlineDiffTableRow from './inline_diff_table_row.vue';
export default {
components: {
diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue
index e0fdbf6ac3a..ab518fcfb16 100644
--- a/app/assets/javascripts/diffs/components/no_changes.vue
+++ b/app/assets/javascripts/diffs/components/no_changes.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlButton, GlSprintf } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index 47eecef2385..3d20dfd0c9b 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -1,7 +1,7 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
-import $ from 'jquery';
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import $ from 'jquery';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 21e0bf18dbf..b167081a379 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -1,11 +1,11 @@
<script>
import { mapGetters, mapState } from 'vuex';
-import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue';
-import parallelDiffTableRow from './parallel_diff_table_row.vue';
+import draftCommentsMixin from '~/diffs/mixins/draft_comments';
+import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
-import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
+import parallelDiffTableRow from './parallel_diff_table_row.vue';
export default {
components: {
diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue
index 2fe2fd6b3d8..7d74e81257a 100644
--- a/app/assets/javascripts/diffs/components/settings_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue
@@ -1,9 +1,9 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButtonGroup, GlButton, GlDropdown, GlFormCheckbox } from '@gitlab/ui';
+import { mapActions, mapGetters, mapState } from 'vuex';
-import eventHub from '../event_hub';
import { EVT_VIEW_FILE_BY_FILE } from '../constants';
+import eventHub from '../event_hub';
import { SETTINGS_DROPDOWN } from '../i18n';
export default {
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 1a258695fa0..39ce849fc03 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { s__, sprintf } from '~/locale';
import FileTree from '~/vue_shared/components/file_tree.vue';
import DiffFileRow from './diff_file_row.vue';
diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js
index c4ac99ead91..2a061876937 100644
--- a/app/assets/javascripts/diffs/i18n.js
+++ b/app/assets/javascripts/diffs/i18n.js
@@ -1,13 +1,16 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const GENERIC_ERROR = __('Something went wrong on our end. Please try again!');
export const DIFF_FILE_HEADER = {
optionsDropdownTitle: __('Options'),
+ fileReviewLabel: __('Viewed'),
+ fileReviewTooltip: __('Collapses this file (only for you) until it’s changed again.'),
};
export const DIFF_FILE = {
- blobView: __('You can %{linkStart}view the blob%{linkEnd} instead.'),
+ tooLarge: s__('MRDiffFile|Changes are too large to be shown.'),
+ blobView: s__('MRDiffFile|View file @ %{commitSha}'),
editInFork: __(
"You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
),
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 4e0720c645a..68fe204d955 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -1,15 +1,14 @@
+import Cookies from 'js-cookie';
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
-import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import eventHub from '../notes/event_hub';
import diffsApp from './components/app.vue';
-import { getDerivedMergeRequestInformation } from './utils/merge_request';
-import { getReviewsForMergeRequest } from './utils/file_reviews';
-
import { TREE_LIST_STORAGE_KEY, DIFF_WHITESPACE_COOKIE_NAME } from './constants';
+import { getReviewsForMergeRequest } from './utils/file_reviews';
+import { getDerivedMergeRequestInformation } from './utils/merge_request';
export default function initDiffsApp(store) {
const fileFinderEl = document.getElementById('js-diff-file-finder');
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index e95e9ac3ee4..4b2dc2d45df 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -1,25 +1,14 @@
-import Vue from 'vue';
import Cookies from 'js-cookie';
-import Poll from '~/lib/utils/poll';
-import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import Vue from 'vue';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { __, s__ } from '~/locale';
+import { diffViewerModes } from '~/ide/constants';
+import axios from '~/lib/utils/axios_utils';
import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils';
+import httpStatusCodes from '~/lib/utils/http_status';
+import Poll from '~/lib/utils/poll';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
-import TreeWorker from '../workers/tree_worker';
+import { __, s__ } from '~/locale';
import notesEventHub from '../../notes/event_hub';
-import eventHub from '../event_hub';
-import {
- getDiffPositionByLineCode,
- getNoteFormData,
- convertExpandLines,
- idleCallback,
- allDiscussionWrappersExpanded,
- prepareDiffData,
- prepareLineForRenamedFile,
-} from './utils';
-import * as types from './mutation_types';
import {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
@@ -48,10 +37,21 @@ import {
DIFF_VIEW_ALL_FILES,
DIFF_FILE_BY_FILE_COOKIE_NAME,
} from '../constants';
-import { diffViewerModes } from '~/ide/constants';
+import eventHub from '../event_hub';
import { isCollapsed } from '../utils/diff_file';
-import { getDerivedMergeRequestInformation } from '../utils/merge_request';
import { markFileReview, setReviewsForMergeRequest } from '../utils/file_reviews';
+import { getDerivedMergeRequestInformation } from '../utils/merge_request';
+import TreeWorker from '../workers/tree_worker';
+import * as types from './mutation_types';
+import {
+ getDiffPositionByLineCode,
+ getNoteFormData,
+ convertExpandLines,
+ idleCallback,
+ allDiscussionWrappersExpanded,
+ prepareDiffData,
+ prepareLineForRenamedFile,
+} from './utils';
export const setBaseConfig = ({ commit }, options) => {
const {
@@ -749,12 +749,10 @@ export const setFileByFile = ({ commit }, { fileByFile }) => {
);
};
-export function reviewFile({ commit, state, getters }, { file, reviewed = true }) {
+export function reviewFile({ commit, state }, { file, reviewed = true }) {
const { mrPath } = getDerivedMergeRequestInformation({ endpoint: file.load_collapsed_diff_url });
- const reviews = setReviewsForMergeRequest(
- mrPath,
- markFileReview(getters.fileReviews(state), file, reviewed),
- );
+ const reviews = markFileReview(state.mrReviews, file, reviewed);
+ setReviewsForMergeRequest(mrPath, reviews);
commit(types.SET_MR_FILE_REVIEWS, reviews);
}
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index a167b6d4694..1fc2a684e95 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,11 +1,11 @@
import { __, n__ } from '~/locale';
-import { parallelizeDiffLines } from './utils';
-import { isFileReviewed } from '../utils/file_reviews';
import {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
INLINE_DIFF_LINES_KEY,
} from '../constants';
+import { computeSuggestionCommitMessage } from '../utils/suggestions';
+import { parallelizeDiffLines } from './utils';
export * from './getters_versions_dropdowns';
@@ -156,6 +156,17 @@ export const diffLines = (state) => (file, unifiedDiffComponents) => {
);
};
-export function fileReviews(state) {
- return state.diffFiles.map((file) => isFileReviewed(state.mrReviews, file));
+export function suggestionCommitMessage(state) {
+ return (values = {}) =>
+ computeSuggestionCommitMessage({
+ message: state.defaultSuggestionCommitMessage,
+ values: {
+ branch_name: state.branchName,
+ project_path: state.projectPath,
+ project_name: state.projectName,
+ username: state.username,
+ user_full_name: state.userFullName,
+ ...values,
+ },
+ });
}
diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
index 3f33b0c900e..01811e60caa 100644
--- a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
+++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
@@ -1,5 +1,5 @@
-import { __, n__, sprintf } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
+import { __, n__, sprintf } from '~/locale';
import { DIFF_COMPARE_BASE_VERSION_INDEX, DIFF_COMPARE_HEAD_VERSION_INDEX } from '../constants';
export const selectedTargetIndex = (state) =>
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index aa89c74cef0..f93435363ec 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -47,4 +47,5 @@ export default () => ({
showSuggestPopover: true,
defaultSuggestionCommitMessage: '',
mrReviews: {},
+ latestDiff: true,
});
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 06f0f2c3dfb..d06793c05af 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -1,6 +1,12 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
+ DIFF_FILE_MANUAL_COLLAPSE,
+ DIFF_FILE_AUTOMATIC_COLLAPSE,
+ INLINE_DIFF_LINES_KEY,
+} from '../constants';
+import * as types from './mutation_types';
+import {
findDiffFile,
addLineReferences,
removeMatchLine,
@@ -9,12 +15,6 @@ import {
isDiscussionApplicableToLine,
updateLineInFile,
} from './utils';
-import {
- DIFF_FILE_MANUAL_COLLAPSE,
- DIFF_FILE_AUTOMATIC_COLLAPSE,
- INLINE_DIFF_LINES_KEY,
-} from '../constants';
-import * as types from './mutation_types';
function updateDiffFilesInState(state, files) {
return Object.assign(state, { diffFiles: files });
@@ -159,7 +159,12 @@ export default {
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
const { latestDiff } = state;
- const discussionLineCodes = [discussion.line_code, ...(discussion.line_codes || [])];
+ const originalStartLineCode = discussion.original_position?.line_range?.start?.line_code;
+ const discussionLineCodes = [
+ discussion.line_code,
+ originalStartLineCode,
+ ...(discussion.line_codes || []),
+ ];
const fileHash = discussion.diff_file.file_hash;
const lineCheck = (line) =>
discussionLineCodes.some(
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index c52da558be2..87b4f33c216 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -1,6 +1,6 @@
import { property, isEqual } from 'lodash';
-import { truncatePathMiddleToLength } from '~/lib/utils/text_utility';
import { diffModes, diffViewerModes } from '~/ide/constants';
+import { truncatePathMiddleToLength } from '~/lib/utils/text_utility';
import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js
index ce0398e75fc..7e6fde320d2 100644
--- a/app/assets/javascripts/diffs/utils/diff_file.js
+++ b/app/assets/javascripts/diffs/utils/diff_file.js
@@ -1,3 +1,5 @@
+import { truncateSha } from '~/lib/utils/text_utility';
+
import {
DIFF_FILE_SYMLINK_MODE,
DIFF_FILE_DELETED_MODE,
@@ -78,3 +80,7 @@ export function isCollapsed(file) {
return collapsedStates[type];
}
+
+export function getShortShaFromFile(file) {
+ return file.content_sha ? truncateSha(String(file.content_sha)) : null;
+}
diff --git a/app/assets/javascripts/diffs/utils/file_reviews.js b/app/assets/javascripts/diffs/utils/file_reviews.js
index 0047955643a..5fafc1714ae 100644
--- a/app/assets/javascripts/diffs/utils/file_reviews.js
+++ b/app/assets/javascripts/diffs/utils/file_reviews.js
@@ -2,6 +2,16 @@ function getFileReviewsKey(mrPath) {
return `${mrPath}-file-reviews`;
}
+export function isFileReviewed(reviews, file) {
+ const fileReviews = reviews[file.file_identifier_hash];
+
+ return file?.id && fileReviews?.length ? new Set(fileReviews).has(file.id) : false;
+}
+
+export function reviewStatuses(files, reviews) {
+ return files.map((file) => isFileReviewed(reviews, file));
+}
+
export function getReviewsForMergeRequest(mrPath) {
const reviewsForMr = localStorage.getItem(getFileReviewsKey(mrPath));
let reviews = {};
@@ -23,23 +33,17 @@ export function setReviewsForMergeRequest(mrPath, reviews) {
return reviews;
}
-export function isFileReviewed(reviews, file) {
- const fileReviews = reviews[file.file_identifier_hash];
-
- return file?.id && fileReviews?.length ? new Set(fileReviews).has(file.id) : false;
-}
-
export function reviewable(file) {
return Boolean(file.id) && Boolean(file.file_identifier_hash);
}
export function markFileReview(reviews, file, reviewed = true) {
const usableReviews = { ...(reviews || {}) };
- let updatedReviews = usableReviews;
+ const updatedReviews = usableReviews;
let fileReviews;
if (reviewable(file)) {
- fileReviews = new Set([...(usableReviews[file.file_identifier_hash] || [])]);
+ fileReviews = new Set(usableReviews[file.file_identifier_hash] || []);
if (reviewed) {
fileReviews.add(file.id);
@@ -47,10 +51,7 @@ export function markFileReview(reviews, file, reviewed = true) {
fileReviews.delete(file.id);
}
- updatedReviews = {
- ...usableReviews,
- [file.file_identifier_hash]: Array.from(fileReviews),
- };
+ updatedReviews[file.file_identifier_hash] = Array.from(fileReviews);
if (updatedReviews[file.file_identifier_hash].length === 0) {
delete updatedReviews[file.file_identifier_hash];
diff --git a/app/assets/javascripts/diffs/utils/performance.js b/app/assets/javascripts/diffs/utils/performance.js
index dcde6f4ecc4..50bf17001a6 100644
--- a/app/assets/javascripts/diffs/utils/performance.js
+++ b/app/assets/javascripts/diffs/utils/performance.js
@@ -9,7 +9,6 @@ import {
MR_DIFFS_MEASURE_DIFF_FILES_DONE,
} from '../../performance/constants';
-import eventHub from '../event_hub';
import {
EVT_PERF_MARK_FILE_TREE_START,
EVT_PERF_MARK_FILE_TREE_END,
@@ -17,6 +16,7 @@ import {
EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
EVT_PERF_MARK_DIFF_FILES_END,
} from '../constants';
+import eventHub from '../event_hub';
function treeStart() {
performanceMarkAndMeasure({
diff --git a/app/assets/javascripts/diffs/utils/suggestions.js b/app/assets/javascripts/diffs/utils/suggestions.js
new file mode 100644
index 00000000000..a272f7f3257
--- /dev/null
+++ b/app/assets/javascripts/diffs/utils/suggestions.js
@@ -0,0 +1,28 @@
+function removeEmptyProperties(dict) {
+ const noBlanks = Object.entries(dict).reduce((final, [key, value]) => {
+ const upd = { ...final };
+
+ // The number 0 shouldn't be falsey when we're printing variables
+ if (value || value === 0) {
+ upd[key] = value;
+ }
+
+ return upd;
+ }, {});
+
+ return noBlanks;
+}
+
+export function computeSuggestionCommitMessage({ message, values = {} } = {}) {
+ const noEmpties = removeEmptyProperties(values);
+ const matchPhrases = Object.keys(noEmpties)
+ .map((key) => `%{${key}}`)
+ .join('|');
+ const replacementExpression = new RegExp(`(${matchPhrases})`, 'gm');
+
+ return message.replace(replacementExpression, (match) => {
+ const key = match.replace(/(^%{|}$)/gm, '');
+
+ return noEmpties[key];
+ });
+}
diff --git a/app/assets/javascripts/diffs/utils/uuids.js b/app/assets/javascripts/diffs/utils/uuids.js
index 1fe5f9f6499..98fe4bf9664 100644
--- a/app/assets/javascripts/diffs/utils/uuids.js
+++ b/app/assets/javascripts/diffs/utils/uuids.js
@@ -12,8 +12,8 @@
*/
import { MersenneTwister } from 'fast-mersenne-twister';
-import stringHash from 'string-hash';
import { isString } from 'lodash';
+import stringHash from 'string-hash';
import { v4 } from 'uuid';
function getSeed(seeds) {
diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
index 54fd5f91194..db13daf0799 100644
--- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js
+++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
@@ -1,5 +1,5 @@
-import { memoize, throttle } from 'lodash';
import $ from 'jquery';
+import { memoize, throttle } from 'lodash';
class DirtySubmitForm {
constructor(form) {
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js
index f4a0b3ed727..05b741af191 100644
--- a/app/assets/javascripts/droplab/drop_down.js
+++ b/app/assets/javascripts/droplab/drop_down.js
@@ -1,5 +1,5 @@
-import utils from './utils';
import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
+import utils from './utils';
class DropDown {
constructor(list, config = {}) {
diff --git a/app/assets/javascripts/droplab/drop_lab.js b/app/assets/javascripts/droplab/drop_lab.js
index 537a05aebb9..74fa6887ba5 100644
--- a/app/assets/javascripts/droplab/drop_lab.js
+++ b/app/assets/javascripts/droplab/drop_lab.js
@@ -1,8 +1,8 @@
+import { DATA_TRIGGER } from './constants';
import HookButton from './hook_button';
import HookInput from './hook_input';
-import utils from './utils';
import Keyboard from './keyboard';
-import { DATA_TRIGGER } from './constants';
+import utils from './utils';
class DropLab {
constructor() {
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index d7aacfbce60..337f7ae2757 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,13 +1,13 @@
-import $ from 'jquery';
import Dropzone from 'dropzone';
+import $ from 'jquery';
import { escape } from 'lodash';
import './behaviors/preview_markdown';
+import { spriteIcon } from '~/lib/utils/common_utils';
+import { getFilename } from '~/lib/utils/file_upload';
+import { n__, __ } from '~/locale';
import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
-import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils';
-import { n__, __ } from '~/locale';
-import { getFilename } from '~/lib/utils/file_upload';
-import { spriteIcon } from '~/lib/utils/common_utils';
+import csrf from './lib/utils/csrf';
Dropzone.autoDiscover = false;
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index c311e1b561c..1f57d73d3d3 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -1,12 +1,12 @@
/* eslint-disable max-classes-per-file */
+import dateFormat from 'dateformat';
import $ from 'jquery';
import Pikaday from 'pikaday';
-import dateFormat from 'dateformat';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { __ } from '~/locale';
+import boardsStore from './boards/stores/boards_store';
import axios from './lib/utils/axios_utils';
import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
-import boardsStore from './boards/stores/boards_store';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 8d1a3d17c6e..d9e6a6c13e2 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -11,6 +11,11 @@ export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
'Editor Lite instance is required to set up an extension.',
);
+export const EDITOR_READY_EVENT = 'editor-ready';
+
+export const EDITOR_TYPE_CODE = 'vs.editor.ICodeEditor';
+export const EDITOR_TYPE_DIFF = 'vs.editor.IDiffEditor';
+
//
// EXTENSIONS' CONSTANTS
//
diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js
index 1808f968b8c..79beb3a4857 100644
--- a/app/assets/javascripts/editor/editor_lite.js
+++ b/app/assets/javascripts/editor/editor_lite.js
@@ -1,12 +1,17 @@
import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
-import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
-import languages from '~/ide/lib/languages';
+import { uuids } from '~/diffs/utils/uuids';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
+import languages from '~/ide/lib/languages';
+import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility';
+import {
+ EDITOR_LITE_INSTANCE_ERROR_NO_EL,
+ URI_PREFIX,
+ EDITOR_READY_EVENT,
+ EDITOR_TYPE_DIFF,
+} from './constants';
import { clearDomElement } from './utils';
-import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from './constants';
-import { uuids } from '~/diffs/utils/uuids';
export default class EditorLite {
constructor(options = {}) {
@@ -29,15 +34,12 @@ export default class EditorLite {
monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
}
- static updateModelLanguage(path, instance) {
- if (!instance) return;
- const model = instance.getModel();
+ static getModelLanguage(path) {
const ext = `.${path.split('.').pop()}`;
const language = monacoLanguages
.getLanguages()
.find((lang) => lang.extensions.indexOf(ext) !== -1);
- const id = language ? language.id : 'plaintext';
- monacoEditor.setModelLanguage(model, id);
+ return language ? language.id : 'plaintext';
}
static pushToImportsArray(arr, toImport) {
@@ -73,50 +75,19 @@ export default class EditorLite {
});
}
- /**
- * Creates a monaco instance with the given options.
- *
- * @param {Object} options Options used to initialize monaco.
- * @param {Element} options.el The element which will be used to create the monacoEditor.
- * @param {string} options.blobPath The path used as the URI of the model. Monaco uses the extension of this path to determine the language.
- * @param {string} options.blobContent The content to initialize the monacoEditor.
- * @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
- */
- createInstance({
- el = undefined,
- blobPath = '',
- blobContent = '',
- blobGlobalId = uuids()[0],
- extensions = [],
- ...instanceOptions
- } = {}) {
+ static prepareInstance(el) {
if (!el) {
throw new Error(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
}
clearDomElement(el);
- const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
-
- const model = monacoEditor.createModel(blobContent, undefined, Uri.file(uriFilePath));
-
monacoEditor.onDidCreateEditor(() => {
delete el.dataset.editorLoading;
});
+ }
- const instance = monacoEditor.create(el, {
- ...this.options,
- ...instanceOptions,
- });
- instance.setModel(model);
- instance.onDidDispose(() => {
- const index = this.instances.findIndex((inst) => inst === instance);
- this.instances.splice(index, 1);
- model.dispose();
- });
- instance.updateModelLanguage = (path) => EditorLite.updateModelLanguage(path, instance);
- instance.use = (args) => this.use(args, instance);
-
+ static manageDefaultExtensions(instance, el, extensions) {
EditorLite.loadExtensions(extensions, instance)
.then((modules) => {
if (modules) {
@@ -126,33 +97,167 @@ export default class EditorLite {
}
})
.then(() => {
- el.dispatchEvent(new Event('editor-ready'));
+ el.dispatchEvent(new Event(EDITOR_READY_EVENT));
})
.catch((e) => {
throw e;
});
+ }
+
+ static createEditorModel({
+ blobPath,
+ blobContent,
+ blobOriginalContent,
+ blobGlobalId,
+ instance,
+ isDiff,
+ } = {}) {
+ if (!instance) {
+ return null;
+ }
+ const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath);
+ const uri = Uri.file(uriFilePath);
+ const existingModel = monacoEditor.getModel(uri);
+ const model = existingModel || monacoEditor.createModel(blobContent, undefined, uri);
+ if (!isDiff) {
+ instance.setModel(model);
+ return model;
+ }
+ const diffModel = {
+ original: monacoEditor.createModel(
+ blobOriginalContent,
+ EditorLite.getModelLanguage(model.uri.path),
+ ),
+ modified: model,
+ };
+ instance.setModel(diffModel);
+ return diffModel;
+ }
+
+ static convertMonacoToELInstance = (inst) => {
+ const editorLiteInstanceAPI = {
+ updateModelLanguage: (path) => {
+ return EditorLite.instanceUpdateLanguage(inst, path);
+ },
+ use: (exts = []) => {
+ return EditorLite.instanceApplyExtension(inst, exts);
+ },
+ };
+ const handler = {
+ get(target, prop, receiver) {
+ if (Reflect.has(editorLiteInstanceAPI, prop)) {
+ return editorLiteInstanceAPI[prop];
+ }
+ return Reflect.get(target, prop, receiver);
+ },
+ };
+ return new Proxy(inst, handler);
+ };
+
+ static instanceUpdateLanguage(inst, path) {
+ const lang = EditorLite.getModelLanguage(path);
+ const model = inst.getModel();
+ return monacoEditor.setModelLanguage(model, lang);
+ }
+
+ static instanceApplyExtension(inst, exts = []) {
+ const extensions = [].concat(exts);
+ extensions.forEach((extension) => {
+ EditorLite.mixIntoInstance(extension, inst);
+ });
+ return inst;
+ }
+
+ static instanceRemoveFromRegistry(editor, instance) {
+ const index = editor.instances.findIndex((inst) => inst === instance);
+ editor.instances.splice(index, 1);
+ }
+
+ static instanceDisposeModels(editor, instance, model) {
+ const instanceModel = instance.getModel() || model;
+ if (!instanceModel) {
+ return;
+ }
+ if (instance.getEditorType() === EDITOR_TYPE_DIFF) {
+ const { original, modified } = instanceModel;
+ if (original) {
+ original.dispose();
+ }
+ if (modified) {
+ modified.dispose();
+ }
+ } else {
+ instanceModel.dispose();
+ }
+ }
+
+ /**
+ * Creates a monaco instance with the given options.
+ *
+ * @param {Object} options Options used to initialize monaco.
+ * @param {Element} options.el The element which will be used to create the monacoEditor.
+ * @param {string} options.blobPath The path used as the URI of the model. Monaco uses the extension of this path to determine the language.
+ * @param {string} options.blobContent The content to initialize the monacoEditor.
+ * @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
+ */
+ createInstance({
+ el = undefined,
+ blobPath = '',
+ blobContent = '',
+ blobOriginalContent = '',
+ blobGlobalId = uuids()[0],
+ extensions = [],
+ isDiff = false,
+ ...instanceOptions
+ } = {}) {
+ EditorLite.prepareInstance(el);
+
+ const createEditorFn = isDiff ? 'createDiffEditor' : 'create';
+ const instance = EditorLite.convertMonacoToELInstance(
+ monacoEditor[createEditorFn].call(this, el, {
+ ...this.options,
+ ...instanceOptions,
+ }),
+ );
+
+ let model;
+ if (instanceOptions.model !== null) {
+ model = EditorLite.createEditorModel({
+ blobGlobalId,
+ blobOriginalContent,
+ blobPath,
+ blobContent,
+ instance,
+ isDiff,
+ });
+ }
+
+ instance.onDidDispose(() => {
+ EditorLite.instanceRemoveFromRegistry(this, instance);
+ EditorLite.instanceDisposeModels(this, instance, model);
+ });
+
+ EditorLite.manageDefaultExtensions(instance, el, extensions);
this.instances.push(instance);
return instance;
}
+ createDiffInstance(args) {
+ return this.createInstance({
+ ...args,
+ isDiff: true,
+ });
+ }
+
dispose() {
this.instances.forEach((instance) => instance.dispose());
}
- use(exts = [], instance = null) {
- const extensions = Array.isArray(exts) ? exts : [exts];
- const initExtensions = (inst) => {
- extensions.forEach((extension) => {
- EditorLite.mixIntoInstance(extension, inst);
- });
- };
- if (instance) {
- initExtensions(instance);
- } else {
- this.instances.forEach((inst) => {
- initExtensions(inst);
- });
- }
+ use(exts) {
+ this.instances.forEach((inst) => {
+ inst.use(exts);
+ });
+ return this;
}
}
diff --git a/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js b/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js
index eb47c20912e..ae6c89d3942 100644
--- a/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js
+++ b/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js
@@ -1,7 +1,7 @@
import Api from '~/api';
import { registerSchema } from '~/ide/utils';
-import { EditorLiteExtension } from './editor_lite_extension_base';
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '../constants';
+import { EditorLiteExtension } from './editor_lite_extension_base';
export class CiSchemaExtension extends EditorLiteExtension {
/**
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index fa1024a74a4..d022fcbeabe 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -1,10 +1,11 @@
-import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { escape, minBy } from 'lodash';
import emojiAliases from 'emojis/aliases.json';
-import axios from '../lib/utils/axios_utils';
import AccessorUtilities from '../lib/utils/accessor';
+import axios from '../lib/utils/axios_utils';
let emojiMap = null;
let validEmojiNames = null;
+export const FALLBACK_EMOJI_KEY = 'grey_question';
export const EMOJI_VERSION = '1';
@@ -30,23 +31,17 @@ async function loadEmoji() {
return data;
}
-async function prepareEmojiMap() {
- emojiMap = await loadEmoji();
+async function loadEmojiWithNames() {
+ return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
+ acc[key] = { ...value, name: key };
- validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
+ return acc;
+ }, {});
+}
- Object.keys(emojiMap).forEach((name) => {
- emojiMap[name].aliases = [];
- emojiMap[name].name = name;
- });
- Object.entries(emojiAliases).forEach(([alias, name]) => {
- // This check, `if (name in emojiMap)` is necessary during testing. In
- // production, it shouldn't be necessary, because at no point should there
- // be an entry in aliases.json with no corresponding entry in emojis.json.
- // However, during testing, the endpoint for emojis.json is mocked with a
- // small dataset, whereas aliases.json is always `import`ed directly.
- if (name in emojiMap) emojiMap[name].aliases.push(alias);
- });
+async function prepareEmojiMap() {
+ emojiMap = await loadEmojiWithNames();
+ validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
}
export function initEmojiMap() {
@@ -63,151 +58,101 @@ export function getValidEmojiNames() {
}
export function isEmojiNameValid(name) {
- return validEmojiNames.indexOf(name) >= 0;
+ if (!emojiMap) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('The emoji map is uninitialized or initialization has not completed');
+ }
+
+ return name in emojiMap || name in emojiAliases;
}
export function getAllEmoji() {
return emojiMap;
}
-/**
- * Retrieves an emoji by name or alias.
- *
- * Note: `initEmojiMap` must have been called and completed before this method
- * can safely be called.
- *
- * @param {String} query The emoji name
- * @param {Boolean} fallback If true, a fallback emoji will be returned if the
- * named emoji does not exist. Defaults to false.
- * @returns {Object} The matching emoji.
- */
-export function getEmoji(query, fallback = false) {
- // TODO https://gitlab.com/gitlab-org/gitlab/-/issues/268208
- const fallbackEmoji = emojiMap.grey_question;
- if (!query) {
- return fallback ? fallbackEmoji : null;
- }
+function getAliasesMatchingQuery(query) {
+ return Object.keys(emojiAliases)
+ .filter((alias) => alias.includes(query))
+ .reduce((map, alias) => {
+ const emojiName = emojiAliases[alias];
+ const score = alias.indexOf(query);
+
+ const prev = map.get(emojiName);
+ // overwrite if we beat the previous score or we're more alphabetical
+ const shouldSet =
+ !prev ||
+ prev.score > score ||
+ (prev.score === score && prev.alias.localeCompare(alias) > 0);
+
+ if (shouldSet) {
+ map.set(emojiName, { score, alias });
+ }
- if (!emojiMap) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('The emoji map is uninitialized or initialization has not completed');
+ return map;
+ }, new Map());
+}
+
+function getUnicodeMatch(emoji, query) {
+ if (emoji.e === query) {
+ return { score: 0, field: 'e', fieldValue: emoji.name, emoji };
}
- const lowercaseQuery = query.toLowerCase();
- const name = normalizeEmojiName(lowercaseQuery);
+ return null;
+}
- if (name in emojiMap) {
- return emojiMap[name];
+function getDescriptionMatch(emoji, query) {
+ if (emoji.d.includes(query)) {
+ return { score: emoji.d.indexOf(query), field: 'd', fieldValue: emoji.d, emoji };
}
- return fallback ? fallbackEmoji : null;
+ return null;
}
-const searchMatchers = {
- // Fuzzy matching compares using a fuzzy matching library
- fuzzy: (value, query) => {
- const score = fuzzaldrinPlus.score(value, query) > 0;
- return { score, success: score > 0 };
- },
- // Contains matching compares by indexOf
- contains: (value, query) => {
- const index = value.indexOf(query.toLowerCase());
- return { index, success: index >= 0 };
- },
- // Exact matching compares by equality
- exact: (value, query) => {
- return { success: value === query.toLowerCase() };
- },
-};
-
-const searchPredicates = {
- // Search by name
- name: (matcher, query) => (emoji) => {
- const m = matcher(emoji.name, query);
- return [{ ...m, emoji, field: emoji.name }];
- },
- // Search by alias
- alias: (matcher, query) => (emoji) =>
- emoji.aliases.map((alias) => {
- const m = matcher(alias, query);
- return { ...m, emoji, field: alias };
- }),
- // Search by description
- description: (matcher, query) => (emoji) => {
- const m = matcher(emoji.d, query);
- return [{ ...m, emoji, field: emoji.d }];
- },
- // Search by unicode value (always exact)
- unicode: (matcher, query) => (emoji) => {
- return [{ emoji, field: emoji.e, success: emoji.e === query }];
- },
-};
+function getAliasMatch(emoji, matchingAliases) {
+ if (matchingAliases.has(emoji.name)) {
+ const { score, alias } = matchingAliases.get(emoji.name);
-/**
- * Searches emoji by name, aliases, description, and unicode value and returns
- * an array of matches.
- *
- * Behavior is undefined if `opts.fields` is empty or if `opts.match` is fuzzy
- * and the query is empty.
- *
- * Note: `initEmojiMap` must have been called and completed before this method
- * can safely be called.
- *
- * @param {String} query Search query.
- * @param {Object} opts Search options (optional).
- * @param {String[]} opts.fields Fields to search. Choices are 'name', 'alias',
- * 'description', and 'unicode' (value). Default is all (four) fields.
- * @param {String} opts.match Search method to use. Choices are 'exact',
- * 'contains', or 'fuzzy'. All methods are case-insensitive. Exact matching (the
- * default) compares by equality. Contains matching compares by indexOf. Fuzzy
- * matching compares using a fuzzy matching library.
- * @param {Boolean} opts.fallback If true, a fallback emoji will be returned if
- * the result set is empty. Defaults to false.
- * @param {Boolean} opts.raw Returns the raw match data instead of just the
- * matching emoji.
- * @returns {Object[]} A list of emoji that match the query.
- */
-export function searchEmoji(query, opts) {
- if (!emojiMap) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('The emoji map is uninitialized or initialization has not completed');
+ return { score, field: 'alias', fieldValue: alias, emoji };
}
- const {
- fields = ['name', 'alias', 'description', 'unicode'],
- match = 'exact',
- fallback = false,
- raw = false,
- } = opts || {};
-
- const fallbackEmoji = emojiMap.grey_question;
- if (!query) {
- if (fallback) {
- return raw ? [{ emoji: fallbackEmoji }] : [fallbackEmoji];
- }
+ return null;
+}
- return [];
+function getNameMatch(emoji, query) {
+ if (emoji.name.includes(query)) {
+ return {
+ score: emoji.name.indexOf(query),
+ field: 'name',
+ fieldValue: emoji.name,
+ emoji,
+ };
}
- // optimization for an exact match in name and alias
- if (match === 'exact' && new Set([...fields, 'name', 'alias']).size === 2) {
- const emoji = getEmoji(query, fallback);
- return emoji ? [emoji] : [];
- }
+ return null;
+}
- const matcher = searchMatchers[match] || searchMatchers.exact;
- const predicates = fields.map((f) => searchPredicates[f](matcher, query));
+export function searchEmoji(query) {
+ const lowercaseQuery = query ? `${query}`.toLowerCase() : '';
- const results = Object.values(emojiMap)
- .flatMap((emoji) => predicates.flatMap((predicate) => predicate(emoji)))
- .filter((r) => r.success);
+ const matchingAliases = getAliasesMatchingQuery(lowercaseQuery);
- // Fallback to question mark for unknown emojis
- if (fallback && results.length === 0) {
- return raw ? [{ emoji: fallbackEmoji }] : [fallbackEmoji];
- }
+ return Object.values(emojiMap)
+ .map((emoji) => {
+ const matches = [
+ getUnicodeMatch(emoji, query),
+ getDescriptionMatch(emoji, lowercaseQuery),
+ getAliasMatch(emoji, matchingAliases),
+ getNameMatch(emoji, lowercaseQuery),
+ ].filter(Boolean);
- return raw ? results : results.map((r) => r.emoji);
+ return minBy(matches, (x) => x.score);
+ })
+ .filter(Boolean);
+}
+
+export function sortEmoji(items) {
+ // Sort results by index of and string comparison
+ return [...items].sort((a, b) => a.score - b.score || a.fieldValue.localeCompare(b.fieldValue));
}
let emojiCategoryMap;
@@ -233,11 +178,28 @@ export function getEmojiCategoryMap() {
return emojiCategoryMap;
}
-export function getEmojiInfo(query) {
- return searchEmoji(query, {
- fields: ['name', 'alias'],
- fallback: true,
- })[0];
+/**
+ * Retrieves an emoji by name
+ *
+ * @param {String} query The emoji name
+ * @param {Boolean} fallback If true, a fallback emoji will be returned if the
+ * named emoji does not exist.
+ * @returns {Object} The matching emoji.
+ */
+export function getEmojiInfo(query, fallback = true) {
+ if (!emojiMap) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('The emoji map is uninitialized or initialization has not completed');
+ }
+
+ const lowercaseQuery = query ? `${query}`.toLowerCase() : '';
+ const name = normalizeEmojiName(lowercaseQuery);
+
+ if (name in emojiMap) {
+ return emojiMap[name];
+ }
+
+ return fallback ? emojiMap[FALLBACK_EMOJI_KEY] : null;
}
export function emojiFallbackImageSrc(inputName) {
@@ -257,12 +219,8 @@ export function glEmojiTag(inputName, options) {
const fallbackSpriteClass = `emoji-${name}`;
const fallbackSpriteAttribute = opts.sprite
- ? `data-fallback-sprite-class="${fallbackSpriteClass}"`
+ ? `data-fallback-sprite-class="${escape(fallbackSpriteClass)}" `
: '';
- return `
- <gl-emoji
- ${fallbackSpriteAttribute}
- data-name="${name}"></gl-emoji>
- `;
+ return `<gl-emoji ${fallbackSpriteAttribute}data-name="${escape(name)}"></gl-emoji>`;
}
diff --git a/app/assets/javascripts/environments/components/canary_ingress.vue b/app/assets/javascripts/environments/components/canary_ingress.vue
index f8cdbb96bc2..02d660a91c1 100644
--- a/app/assets/javascripts/environments/components/canary_ingress.vue
+++ b/app/assets/javascripts/environments/components/canary_ingress.vue
@@ -1,6 +1,6 @@
<script>
-import { uniqueId } from 'lodash';
import { GlDropdown, GlDropdownItem, GlModalDirective as GlModal } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
import { s__ } from '~/locale';
import { CANARY_UPDATE_MODAL } from '../constants';
diff --git a/app/assets/javascripts/environments/components/canary_update_modal.vue b/app/assets/javascripts/environments/components/canary_update_modal.vue
index fc63d6272c8..8b1121c7158 100644
--- a/app/assets/javascripts/environments/components/canary_update_modal.vue
+++ b/app/assets/javascripts/environments/components/canary_update_modal.vue
@@ -1,8 +1,8 @@
<script>
import { GlAlert, GlModal, GlSprintf } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import updateCanaryIngress from '../graphql/mutations/update_canary_ingress.mutation.graphql';
import { CANARY_UPDATE_MODAL } from '../constants';
+import updateCanaryIngress from '../graphql/mutations/update_canary_ingress.mutation.graphql';
export default {
components: {
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index b2a8571820b..76ad74e04d0 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -4,8 +4,8 @@
* Render modal to confirm rollback/redeploy.
*/
-import { escape } from 'lodash';
import { GlModal } from '@gitlab/ui';
+import { escape } from 'lodash';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index c6b34fecbb7..9e058af56c4 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -26,26 +26,6 @@ export default {
type: Boolean,
required: true,
},
- deployBoardsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
},
methods: {
onChangePage(page) {
@@ -62,14 +42,7 @@ export default {
<slot name="empty-state"></slot>
<div v-if="!isLoading && environments.length > 0" class="table-holder">
- <environment-table
- :environments="environments"
- :can-read-environment="canReadEnvironment"
- :user-callouts-path="userCalloutsPath"
- :lock-promotion-svg-path="lockPromotionSvgPath"
- :help-canary-deployments-path="helpCanaryDeploymentsPath"
- :deploy-boards-help-path="deployBoardsHelpPath"
- />
+ <environment-table :environments="environments" :can-read-environment="canReadEnvironment" />
<table-pagination
v-if="pagination && pagination.totalPages > 1"
diff --git a/app/assets/javascripts/environments/components/deploy_board.vue b/app/assets/javascripts/environments/components/deploy_board.vue
index 07cb968d8d3..f9c4660036b 100644
--- a/app/assets/javascripts/environments/components/deploy_board.vue
+++ b/app/assets/javascripts/environments/components/deploy_board.vue
@@ -9,7 +9,7 @@
* - Button Actions.
* [Mockup](https://gitlab.com/gitlab-org/gitlab-foss/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png)
*/
-import { isEmpty } from 'lodash';
+import deployBoardSvg from '@gitlab/svgs/dist/illustrations/deploy-boards.svg';
import {
GlIcon,
GlLoadingIcon,
@@ -18,9 +18,9 @@ import {
GlTooltipDirective,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
-import deployBoardSvg from '@gitlab/svgs/dist/illustrations/deploy-boards.svg';
-import instanceComponent from '~/vue_shared/components/deployment_instance.vue';
+import { isEmpty } from 'lodash';
import { n__ } from '~/locale';
+import instanceComponent from '~/vue_shared/components/deployment_instance.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { STATUS_MAP, CANARY_STATUS } from '../constants';
import CanaryIngress from './canary_ingress.vue';
@@ -44,11 +44,6 @@ export default {
type: Object,
required: true,
},
- deployBoardsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
isLoading: {
type: Boolean,
required: true,
diff --git a/app/assets/javascripts/environments/components/enable_review_app_modal.vue b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
index 3dd1d5a1bcc..2494968857c 100644
--- a/app/assets/javascripts/environments/components/enable_review_app_modal.vue
+++ b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
export default {
components: {
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 2192d456861..8911380290a 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility';
+import { __, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
diff --git a/app/assets/javascripts/environments/components/environment_delete.vue b/app/assets/javascripts/environments/components/environment_delete.vue
index 75d92d3295d..4b7917b4572 100644
--- a/app/assets/javascripts/environments/components/environment_delete.vue
+++ b/app/assets/javascripts/environments/components/environment_delete.vue
@@ -5,6 +5,7 @@
*/
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
@@ -40,7 +41,7 @@ export default {
},
methods: {
onClick() {
- this.$root.$emit('bv::hide::tooltip', this.$options.deleteEnvironmentTooltipId);
+ this.$root.$emit(BV_HIDE_TOOLTIP, this.$options.deleteEnvironmentTooltipId);
eventHub.$emit('requestDeleteEnvironment', this.environment);
},
onDeleteEnvironment(environment) {
@@ -59,7 +60,7 @@ export default {
:loading="isLoading"
:title="title"
:aria-label="title"
- class="gl-display-none gl-display-md-block"
+ class="gl-display-none gl-md-display-block"
variant="danger"
category="primary"
icon="remove"
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 1724cc692bd..4db0dff16aa 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,22 +1,22 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { isEmpty } from 'lodash';
import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
+import { isEmpty } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { __, s__, sprintf } from '~/locale';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
import eventHub from '../event_hub';
import ActionsComponent from './environment_actions.vue';
+import DeleteComponent from './environment_delete.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import PinComponent from './environment_pin.vue';
-import DeleteComponent from './environment_delete.vue';
-import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue';
+import StopComponent from './environment_stop.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
/**
@@ -549,7 +549,7 @@ export default {
upcomingDeploymentCellClasses() {
return [
this.tableData.upcoming.spacing,
- { 'gl-display-none gl-display-md-block': !this.upcomingDeployment },
+ { 'gl-display-none gl-md-display-block': !this.upcomingDeployment },
];
},
},
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 4dc2c0ec1bd..7f70433776d 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -30,7 +30,7 @@ export default {
:href="monitoringUrl"
:title="title"
:aria-label="title"
- class="monitoring-url gl-display-none gl-display-sm-none gl-display-md-block"
+ class="monitoring-url gl-display-none gl-sm-display-none gl-md-display-block"
icon="chart"
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 48edde82ce7..397616c654f 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -68,7 +68,7 @@ export default {
<gl-button
v-gl-tooltip
v-gl-modal.confirm-rollback-modal
- class="gl-display-none gl-display-md-block text-secondary"
+ class="gl-display-none gl-md-display-block text-secondary"
:loading="isLoading"
:title="title"
:icon="isLastDeployment ? 'repeat' : 'redo'"
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index 8e100623199..dceaf3cacf1 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -5,6 +5,7 @@
*/
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
@@ -40,7 +41,7 @@ export default {
},
methods: {
onClick() {
- this.$root.$emit('bv::hide::tooltip', this.$options.stopEnvironmentTooltipId);
+ this.$root.$emit(BV_HIDE_TOOLTIP, this.$options.stopEnvironmentTooltipId);
eventHub.$emit('requestStopEnvironment', this.environment);
},
onStopEnvironment(environment) {
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 6f68c6e864a..ed852895f10 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -2,14 +2,14 @@
import { GlBadge, GlButton, GlModalDirective, GlTab, GlTabs } from '@gitlab/ui';
import { deprecatedCreateFlash as 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 EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
+import ConfirmRollbackModal from './confirm_rollback_modal.vue';
+import DeleteEnvironmentModal from './delete_environment_modal.vue';
+import emptyState from './empty_state.vue';
import EnableReviewAppModal from './enable_review_app_modal.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
-import DeleteEnvironmentModal from './delete_environment_modal.vue';
-import ConfirmRollbackModal from './confirm_rollback_modal.vue';
export default {
i18n: {
@@ -33,7 +33,7 @@ export default {
directives: {
'gl-modal': GlModalDirective,
},
- mixins: [CIPaginationMixin, environmentsMixin],
+ mixins: [EnvironmentsPaginationApiMixin, environmentsMixin],
props: {
endpoint: {
type: String,
@@ -51,30 +51,10 @@ export default {
type: String,
required: true,
},
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
helpPagePath: {
type: String,
required: true,
},
- deployBoardsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
},
created() {
@@ -133,7 +113,7 @@ export default {
<confirm-rollback-modal :environment="environmentInRollbackModal" />
<div class="gl-w-full">
- <div class="gl-display-flex gl-flex-direction-column gl-mt-3 gl-display-md-none!">
+ <div class="gl-display-flex gl-flex-direction-column gl-mt-3 gl-md-display-none!">
<gl-button
v-if="state.reviewAppDetails.can_setup_review_app"
v-gl-modal="$options.modal.id"
@@ -167,7 +147,7 @@ export default {
</gl-tab>
<template #tabs-end>
<div
- class="gl-display-none gl-display-md-flex gl-lg-align-items-center gl-lg-flex-direction-row gl-lg-flex-fill-1 gl-lg-justify-content-end gl-lg-mt-0"
+ class="gl-display-none gl-md-display-flex gl-lg-align-items-center gl-lg-flex-direction-row gl-lg-flex-fill-1 gl-lg-justify-content-end gl-lg-mt-0"
>
<gl-button
v-if="state.reviewAppDetails.can_setup_review_app"
@@ -195,10 +175,6 @@ export default {
:environments="state.environments"
:pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
- :user-callouts-path="userCalloutsPath"
- :lock-promotion-svg-path="lockPromotionSvgPath"
- :help-canary-deployments-path="helpCanaryDeploymentsPath"
- :deploy-boards-help-path="deployBoardsHelpPath"
@onChangePage="onChangePage"
>
<template v-if="!isLoading && state.environments.length === 0" #empty-state>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index bbb56ca6f26..cbce887f491 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -5,9 +5,9 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { flow, reverse, sortBy } from 'lodash/fp';
import { s__ } from '~/locale';
-import EnvironmentItem from './environment_item.vue';
-import DeployBoard from './deploy_board.vue';
import CanaryUpdateModal from './canary_update_modal.vue';
+import DeployBoard from './deploy_board.vue';
+import EnvironmentItem from './environment_item.vue';
export default {
components: {
@@ -23,31 +23,11 @@ export default {
required: true,
default: () => [],
},
- deployBoardsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
@@ -189,7 +169,6 @@ export default {
<div class="deploy-board-container">
<deploy-board
:deploy-board-data="model.deployBoardData"
- :deploy-boards-help-path="deployBoardsHelpPath"
:is-loading="model.isLoadingDeployBoard"
:is-empty="model.isEmptyDeployBoard"
:logs-path="model.logs_path"
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index 0832822520d..828a7098b36 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 { GlSprintf, GlTooltipDirective, GlModal } from '@gitlab/ui';
-import eventHub from '../event_hub';
import { __, s__ } from '~/locale';
+import eventHub from '../event_hub';
export default {
id: 'stop-environment-modal',
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index e4726412f99..1be9a4608cb 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import environmentsFolderApp from './environments_folder_view.vue';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
-import createDefaultClient from '~/lib/graphql';
+import environmentsFolderApp from './environments_folder_view.vue';
Vue.use(Translate);
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index dbb60fa4622..8070f3f12f8 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,9 +1,9 @@
<script>
import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
-import environmentsMixin from '../mixins/environments_mixin';
-import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
-import StopEnvironmentModal from '../components/stop_environment_modal.vue';
import DeleteEnvironmentModal from '../components/delete_environment_modal.vue';
+import StopEnvironmentModal from '../components/stop_environment_modal.vue';
+import environmentsMixin from '../mixins/environments_mixin';
+import EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
export default {
components: {
@@ -14,7 +14,7 @@ export default {
StopEnvironmentModal,
},
- mixins: [environmentsMixin, CIPaginationMixin],
+ mixins: [environmentsMixin, EnvironmentsPaginationApiMixin],
props: {
endpoint: {
@@ -34,21 +34,6 @@ export default {
type: Boolean,
required: true,
},
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
},
methods: {
successCallback(resp) {
@@ -88,9 +73,6 @@ export default {
:environments="state.environments"
:pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
- :user-callouts-path="userCalloutsPath"
- :lock-promotion-svg-path="lockPromotionSvgPath"
- :help-canary-deployments-path="helpCanaryDeploymentsPath"
@onChangePage="onChangePage"
/>
</div>
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index 4d734a457ab..68348648e61 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import environmentsComponent from './components/environments_app.vue';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
-import createDefaultClient from '~/lib/graphql';
+import environmentsComponent from './components/environments_app.vue';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -30,7 +30,6 @@ export default () => {
endpoint: environmentsData.environmentsDataEndpoint,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
- deployBoardsHelpPath: environmentsData.deployBoardsHelpPath,
canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
};
@@ -41,7 +40,6 @@ export default () => {
endpoint: this.endpoint,
newEnvironmentPath: this.newEnvironmentPath,
helpPagePath: this.helpPagePath,
- deployBoardsHelpPath: this.deployBoardsHelpPath,
canCreateEnvironment: this.canCreateEnvironment,
canReadEnvironment: this.canReadEnvironment,
},
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 15a00c11ee6..d5caff1660a 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -3,18 +3,18 @@
*/
import { isEqual, isFunction, omitBy } from 'lodash';
import Visibility from 'visibilityjs';
-import EnvironmentsStore from '../stores/environments_store';
-import Poll from '../../lib/utils/poll';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import { getParameterByName } from '../../lib/utils/common_utils';
+import Poll from '../../lib/utils/poll';
import { s__ } from '../../locale';
-import { deprecatedCreateFlash as Flash } from '../../flash';
+import tabs from '../../vue_shared/components/navigation_tabs.vue';
+import tablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
+import container from '../components/container.vue';
+import environmentTable from '../components/environments_table.vue';
import eventHub from '../event_hub';
import EnvironmentsService from '../services/environments_service';
-import tablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
-import environmentTable from '../components/environments_table.vue';
-import tabs from '../../vue_shared/components/navigation_tabs.vue';
-import container from '../components/container.vue';
+import EnvironmentsStore from '../stores/environments_store';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js
index be04ff158e7..b62fe196a6f 100644
--- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js
@@ -1,11 +1,10 @@
/**
* API callbacks for pagination and tabs
- * shared between Pipelines and Environments table.
*
* Components need to have `scope`, `page` and `requestData`
*/
-import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils';
import { validateParams } from '~/pipelines/utils';
+import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils';
export default {
methods: {
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 8911885e920..f7fdbb03f04 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,5 +1,5 @@
-import { setDeployBoard } from './helpers';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { setDeployBoard } from './helpers';
/**
* Environments Store.
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index e21c6b62b91..03b8df50c54 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import {
GlButton,
GlFormInput,
@@ -13,21 +12,22 @@ import {
GlDropdownDivider,
GlIcon,
} from '@gitlab/ui';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, sprintf, n__ } from '~/locale';
+import Tracking from '~/tracking';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
-import Stacktrace from './stacktrace.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import { severityLevel, severityLevelVariant, errorStatus } from './constants';
-import Tracking from '~/tracking';
+import query from '../queries/details.query.graphql';
import {
trackClickErrorLinkToSentryOptions,
trackErrorDetailsViewsOptions,
trackErrorStatusUpdateOptions,
} from '../utils';
-import query from '../queries/details.query.graphql';
+import { severityLevel, severityLevelVariant, errorStatus } from './constants';
+import Stacktrace from './stacktrace.vue';
const SENTRY_TIMEOUT = 10000;
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 7ccb6253508..0f564fc3c60 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapState } from 'vuex';
import {
GlEmptyState,
GlButton,
@@ -15,12 +14,14 @@ import {
GlPagination,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
+import { mapActions, mapState } from 'vuex';
+import { helpPagePath } from '~/helpers/help_page_helper';
import AccessorUtils from '~/lib/utils/accessor';
-import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
-import ErrorTrackingActions from './error_tracking_actions.vue';
import Tracking from '~/tracking';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '../utils';
+import ErrorTrackingActions from './error_tracking_actions.vue';
export const tableDataClass = 'table-col d-flex d-md-table-cell align-items-center';
@@ -138,6 +139,9 @@ export default {
paginationRequired() {
return !isEmpty(this.pagination);
},
+ errorTrackingHelpUrl() {
+ return helpPagePath('operations/error_tracking');
+ },
},
watch: {
pagination() {
@@ -404,7 +408,7 @@ export default {
<template #description>
<div>
<span>{{ __('Monitor your errors by integrating with Sentry.') }}</span>
- <gl-link target="_blank" href="/help/user/project/operations/error_tracking.html">{{
+ <gl-link target="_blank" :href="errorTrackingHelpUrl">{{
__('More information')
}}</gl-link>
</div>
diff --git a/app/assets/javascripts/error_tracking/details.js b/app/assets/javascripts/error_tracking/details.js
index 55ab362f805..37b8007d556 100644
--- a/app/assets/javascripts/error_tracking/details.js
+++ b/app/assets/javascripts/error_tracking/details.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import store from './store';
-import ErrorDetails from './components/error_details.vue';
import csrf from '~/lib/utils/csrf';
+import ErrorDetails from './components/error_details.vue';
+import store from './store';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/error_tracking/list.js b/app/assets/javascripts/error_tracking/list.js
index cb656a9ef13..9c729407009 100644
--- a/app/assets/javascripts/error_tracking/list.js
+++ b/app/assets/javascripts/error_tracking/list.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
-import store from './store';
import ErrorTrackingList from './components/error_tracking_list.vue';
+import store from './store';
export default () => {
const selector = '#js-error_tracking';
diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js
index 8f1e7e0b959..a27ebd16956 100644
--- a/app/assets/javascripts/error_tracking/store/actions.js
+++ b/app/assets/javascripts/error_tracking/store/actions.js
@@ -1,8 +1,8 @@
-import service from '../services';
-import * as types from './mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import service from '../services';
+import * as types from './mutation_types';
export const setStatus = ({ commit }, status) => {
commit(types.SET_ERROR_STATUS, status.toLowerCase());
diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js
index 394dec938cf..7319d45bbd2 100644
--- a/app/assets/javascripts/error_tracking/store/details/actions.js
+++ b/app/assets/javascripts/error_tracking/store/details/actions.js
@@ -1,8 +1,8 @@
-import service from '../../services';
-import * as types from './mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
+import service from '../../services';
+import * as types from './mutation_types';
let stackTracePoll;
diff --git a/app/assets/javascripts/error_tracking/store/index.js b/app/assets/javascripts/error_tracking/store/index.js
index d9206bc8d7c..1d8f1998583 100644
--- a/app/assets/javascripts/error_tracking/store/index.js
+++ b/app/assets/javascripts/error_tracking/store/index.js
@@ -2,16 +2,16 @@ import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
-import mutations from './mutations';
+import * as detailsActions from './details/actions';
+import * as detailsGetters from './details/getters';
+import detailsMutations from './details/mutations';
+import detailsState from './details/state';
import * as listActions from './list/actions';
import listMutations from './list/mutations';
import listState from './list/state';
-import * as detailsActions from './details/actions';
-import detailsMutations from './details/mutations';
-import detailsState from './details/state';
-import * as detailsGetters from './details/getters';
+import mutations from './mutations';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index a242c0e4236..f07e546241a 100644
--- a/app/assets/javascripts/error_tracking/store/list/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/actions.js
@@ -1,8 +1,8 @@
-import Service from '../../services';
-import * as types from './mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
+import Service from '../../services';
+import * as types from './mutation_types';
let eTagPoll;
diff --git a/app/assets/javascripts/error_tracking/store/list/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js
index 84a62fa9024..d92a64947ad 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutations.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutations.js
@@ -1,6 +1,6 @@
-import * as types from './mutation_types';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import AccessorUtils from '~/lib/utils/accessor';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import * as types from './mutation_types';
export default {
[types.SET_ERRORS](state, data) {
diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue
index 786abc8ce49..971eb21ee3b 100644
--- a/app/assets/javascripts/error_tracking_settings/components/app.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/app.vue
@@ -1,8 +1,8 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
-import ProjectDropdown from './project_dropdown.vue';
+import { mapActions, mapGetters, mapState } from 'vuex';
import ErrorTrackingForm from './error_tracking_form.vue';
+import ProjectDropdown from './project_dropdown.vue';
export default {
components: { ProjectDropdown, ErrorTrackingForm, GlButton },
diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
index b1b699d2e2a..dbcda0877b4 100644
--- a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlFormInput, GlIcon, GlButton } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
export default {
components: { GlFormInput, GlIcon, GlButton },
diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js
index 2821798f82d..7eb684fb52c 100644
--- a/app/assets/javascripts/error_tracking_settings/store/actions.js
+++ b/app/assets/javascripts/error_tracking_settings/store/actions.js
@@ -1,7 +1,7 @@
-import { __ } from '~/locale';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { __ } from '~/locale';
import { transformFrontendSettings } from '../utils';
import * as types from './mutation_types';
diff --git a/app/assets/javascripts/error_tracking_settings/store/index.js b/app/assets/javascripts/error_tracking_settings/store/index.js
index 560f265a2ea..1cd6d119657 100644
--- a/app/assets/javascripts/error_tracking_settings/store/index.js
+++ b/app/assets/javascripts/error_tracking_settings/store/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import createState from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
+import createState from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/error_tracking_settings/store/mutations.js b/app/assets/javascripts/error_tracking_settings/store/mutations.js
index 1fc028093c1..2242169aa1e 100644
--- a/app/assets/javascripts/error_tracking_settings/store/mutations.js
+++ b/app/assets/javascripts/error_tracking_settings/store/mutations.js
@@ -1,7 +1,7 @@
import { pick } from 'lodash';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
-import * as types from './mutation_types';
import { projectKeys } from '../utils';
+import * as types from './mutation_types';
export default {
[types.CLEAR_PROJECTS](state) {
diff --git a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
index 210212fa900..b1e60066e11 100644
--- a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
+++ b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
@@ -91,9 +91,9 @@ export default {
<h3 class="page-title gl-m-0">{{ title }}</h3>
</div>
- <div v-if="error.length" class="alert alert-danger">
+ <gl-alert v-if="error.length" variant="warning" class="gl-mb-5" :dismissible="false">
<p v-for="(message, index) in error" :key="index" class="gl-mb-0">{{ message }}</p>
- </div>
+ </gl-alert>
<feature-flag-form
:name="name"
diff --git a/app/assets/javascripts/feature_flags/components/environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/environments_dropdown.vue
index 88f1e692f5f..7f316d20f9c 100644
--- a/app/assets/javascripts/feature_flags/components/environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/environments_dropdown.vue
@@ -1,9 +1,9 @@
<script>
-import { debounce } from 'lodash';
import { GlButton, GlSearchBoxByType } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
/**
* Creates a searchable input for environments.
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue
index ddeefd7b827..348b71dc1c6 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue
@@ -1,20 +1,19 @@
<script>
-import { mapState, mapActions } from 'vuex';
-import { isEmpty } from 'lodash';
import { GlAlert, GlButton, GlModalDirective, GlSprintf, GlTabs } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
+import { mapState, mapActions } from 'vuex';
-import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants';
-import FeatureFlagsTab from './feature_flags_tab.vue';
-import FeatureFlagsTable from './feature_flags_table.vue';
-import UserListsTable from './user_lists_table.vue';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import {
buildUrlWithCurrentLocation,
getParameterByName,
historyPushState,
} from '~/lib/utils/common_utils';
-
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants';
import ConfigureFeatureFlagsModal from './configure_feature_flags_modal.vue';
+import FeatureFlagsTab from './feature_flags_tab.vue';
+import FeatureFlagsTable from './feature_flags_table.vue';
+import UserListsTable from './user_lists_table.vue';
const SCOPES = { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE };
@@ -198,7 +197,7 @@ export default {
@token="rotateInstanceId()"
/>
<div :class="topAreaBaseClasses">
- <div class="gl-display-flex gl-flex-direction-column gl-display-md-none!">
+ <div class="gl-display-flex gl-flex-direction-column gl-md-display-none!">
<gl-button
v-if="canUserConfigure"
v-gl-modal="'configure-feature-flags'"
@@ -285,7 +284,7 @@ export default {
</feature-flags-tab>
<template #tabs-end>
<li
- class="gl-display-none gl-display-md-flex gl-align-items-center gl-flex-fill-1 gl-justify-content-end"
+ class="gl-display-none gl-md-display-flex gl-align-items-center gl-flex-fill-1 gl-justify-content-end"
>
<gl-button
v-if="canUserConfigure"
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue b/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue
index 24b0b54d1be..d0df00e446b 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue
@@ -68,41 +68,39 @@ export default {
<span data-testid="feature-flags-tab-title">{{ title }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ itemCount }}</gl-badge>
</template>
- <template>
- <gl-alert
- v-for="(message, index) in alerts"
- :key="index"
- data-testid="serverErrors"
- variant="danger"
- @dismiss="clearAlert(index)"
- >
- {{ message }}
- </gl-alert>
+ <gl-alert
+ v-for="(message, index) in alerts"
+ :key="index"
+ data-testid="serverErrors"
+ variant="danger"
+ @dismiss="clearAlert(index)"
+ >
+ {{ message }}
+ </gl-alert>
- <gl-loading-icon v-if="isLoading" :label="loadingLabel" size="md" class="gl-mt-4" />
+ <gl-loading-icon v-if="isLoading" :label="loadingLabel" size="md" class="gl-mt-4" />
- <gl-empty-state
- v-else-if="errorState"
- :title="errorTitle"
- :description="s__(`FeatureFlags|Try again in a few moments or contact your support team.`)"
- :svg-path="errorStateSvgPath"
- data-testid="error-state"
- />
+ <gl-empty-state
+ v-else-if="errorState"
+ :title="errorTitle"
+ :description="s__(`FeatureFlags|Try again in a few moments or contact your support team.`)"
+ :svg-path="errorStateSvgPath"
+ data-testid="error-state"
+ />
- <gl-empty-state
- v-else-if="emptyState"
- :title="emptyTitle"
- :svg-path="errorStateSvgPath"
- data-testid="empty-state"
- >
- <template #description>
- {{ emptyDescription }}
- <gl-link :href="featureFlagsHelpPagePath" target="_blank">
- {{ s__('FeatureFlags|More information') }}
- </gl-link>
- </template>
- </gl-empty-state>
- <slot> </slot>
- </template>
+ <gl-empty-state
+ v-else-if="emptyState"
+ :title="emptyTitle"
+ :svg-path="errorStateSvgPath"
+ data-testid="empty-state"
+ >
+ <template #description>
+ {{ emptyDescription }}
+ <gl-link :href="featureFlagsHelpPagePath" target="_blank">
+ {{ s__('FeatureFlags|More information') }}
+ </gl-link>
+ </template>
+ </gl-empty-state>
+ <slot> </slot>
</gl-tab>
</template>
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
index f3b199b5aca..04a5e5bc3c5 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
@@ -199,7 +199,7 @@ export default {
:key="strategy.id"
data-testid="strategy-badge"
variant="info"
- class="gl-mr-3 gl-mt-2"
+ class="gl-mr-3 gl-mt-2 gl-white-space-normal gl-text-left gl-px-5"
>{{ strategyBadgeText(strategy) }}</gl-badge
>
</template>
diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue
index 253661ece1f..f6a14d9996f 100644
--- a/app/assets/javascripts/feature_flags/components/form.vue
+++ b/app/assets/javascripts/feature_flags/components/form.vue
@@ -1,6 +1,4 @@
<script>
-import Vue from 'vue';
-import { memoize, isString, cloneDeep, isNumber, uniqueId } from 'lodash';
import {
GlButton,
GlBadge,
@@ -10,13 +8,13 @@ import {
GlFormCheckbox,
GlSprintf,
GlIcon,
+ GlToggle,
} from '@gitlab/ui';
-import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
+import { memoize, isString, cloneDeep, isNumber, uniqueId } from 'lodash';
+import Vue from 'vue';
import { s__ } from '~/locale';
+import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
-import EnvironmentsDropdown from './environments_dropdown.vue';
-import Strategy from './strategy.vue';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
@@ -27,6 +25,8 @@ import {
LEGACY_FLAG,
} from '../constants';
import { createNewEnvironmentScope } from '../store/helpers';
+import EnvironmentsDropdown from './environments_dropdown.vue';
+import Strategy from './strategy.vue';
export default {
components: {
@@ -37,7 +37,7 @@ export default {
GlTooltip,
GlSprintf,
GlIcon,
- ToggleButton,
+ GlToggle,
EnvironmentsDropdown,
Strategy,
RelatedIssuesRoot,
@@ -372,7 +372,7 @@ export default {
{{ s__('FeatureFlags|Environment Spec') }}
</div>
<div
- class="table-mobile-content js-feature-flag-status d-flex align-items-center justify-content-start"
+ class="table-mobile-content gl-display-flex gl-align-items-center gl-justify-content-start"
>
<p v-if="isAllEnvironment(scope.environmentScope)" class="js-scope-all pl-3">
{{ $options.translations.allEnvironmentsText }}
@@ -398,10 +398,10 @@ export default {
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Status') }}
</div>
- <div class="table-mobile-content js-feature-flag-status">
- <toggle-button
+ <div class="table-mobile-content gl-display-flex gl-justify-content-center">
+ <gl-toggle
:value="scope.active"
- :disabled-input="!active || !canUpdateScope(scope)"
+ :disabled="!active || !canUpdateScope(scope)"
@change="(status) => (scope.active = status)"
/>
</div>
@@ -498,25 +498,26 @@ export default {
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Remove') }}
</div>
- <div class="table-mobile-content js-feature-flag-delete">
+ <div class="table-mobile-content">
<gl-button
v-if="!isAllEnvironment(scope.environmentScope) && canUpdateScope(scope)"
v-gl-tooltip
:title="s__('FeatureFlags|Remove')"
class="js-delete-scope btn-transparent pr-3 pl-3"
icon="clear"
+ data-testid="feature-flag-delete"
@click="removeScope(scope)"
/>
</div>
</div>
</div>
- <div class="js-add-new-scope gl-responsive-table-row" role="row">
+ <div class="gl-responsive-table-row" role="row" data-testid="add-new-scope">
<div class="table-section section-30" role="gridcell">
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Environment Spec') }}
</div>
- <div class="table-mobile-content js-feature-flag-status">
+ <div class="table-mobile-content">
<environments-dropdown
class="js-new-scope-name col-12"
:value="newScope"
@@ -530,9 +531,9 @@ export default {
<div class="table-mobile-header" role="rowheader">
{{ s__('FeatureFlags|Status') }}
</div>
- <div class="table-mobile-content js-feature-flag-status">
- <toggle-button
- :disabled-input="!active"
+ <div class="table-mobile-content gl-display-flex gl-justify-content-center">
+ <gl-toggle
+ :disabled="!active"
:value="false"
@change="createNewScope({ active: true })"
/>
diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
index f2017c22abf..efe4ff71a9e 100644
--- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
@@ -1,5 +1,4 @@
<script>
-import { debounce } from 'lodash';
import {
GlDropdown,
GlDropdownDivider,
@@ -8,9 +7,10 @@ import {
GlLoadingIcon,
GlSearchBoxByType,
} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
export default {
components: {
diff --git a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
index 529fefd7e45..19be57f9d27 100644
--- a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
+++ b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
@@ -1,15 +1,16 @@
<script>
+import { GlAlert } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import axios from '~/lib/utils/axios_utils';
-import FeatureFlagForm from './form.vue';
+import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { NEW_VERSION_FLAG, ROLLOUT_STRATEGY_ALL_USERS } from '../constants';
import { createNewEnvironmentScope } from '../store/helpers';
-
-import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import FeatureFlagForm from './form.vue';
export default {
components: {
FeatureFlagForm,
+ GlAlert,
},
mixins: [featureFlagsMixin()],
inject: {
@@ -61,9 +62,9 @@ export default {
<div>
<h3 class="page-title">{{ s__('FeatureFlags|New feature flag') }}</h3>
- <div v-if="error.length" class="alert alert-danger">
- <p v-for="(message, index) in error" :key="index" class="mb-0">{{ message }}</p>
- </div>
+ <gl-alert v-if="error.length" variant="warning" class="gl-mb-5" :dismissible="false">
+ <p v-for="(message, index) in error" :key="index" class="gl-mb-0">{{ message }}</p>
+ </gl-alert>
<feature-flag-form
:cancel-path="path"
diff --git a/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue b/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue
index 6a57e9a8759..45fc37da747 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue
@@ -1,7 +1,7 @@
<script>
+import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { debounce } from 'lodash';
import { createNamespacedHelpers } from 'vuex';
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { s__ } from '~/locale';
import ParameterFormGroup from './parameter_form_group.vue';
diff --git a/app/assets/javascripts/feature_flags/components/strategies/parameter_form_group.vue b/app/assets/javascripts/feature_flags/components/strategies/parameter_form_group.vue
index 7f2c6d55db8..822ff92b405 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/parameter_form_group.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/parameter_form_group.vue
@@ -1,6 +1,6 @@
<script>
-import { uniqueId } from 'lodash';
import { GlFormGroup } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
export default {
components: {
diff --git a/app/assets/javascripts/feature_flags/components/strategy.vue b/app/assets/javascripts/feature_flags/components/strategy.vue
index 9593bcf6487..170f120b036 100644
--- a/app/assets/javascripts/feature_flags/components/strategy.vue
+++ b/app/assets/javascripts/feature_flags/components/strategy.vue
@@ -1,7 +1,7 @@
<script>
-import Vue from 'vue';
-import { isNumber } from 'lodash';
import { GlAlert, GlButton, GlFormSelect, GlFormGroup, GlIcon, GlLink, GlToken } from '@gitlab/ui';
+import { isNumber } from 'lodash';
+import Vue from 'vue';
import { s__, __ } from '~/locale';
import {
EMPTY_PARAMETERS,
diff --git a/app/assets/javascripts/feature_flags/components/strategy_parameters.vue b/app/assets/javascripts/feature_flags/components/strategy_parameters.vue
index a22f081bb92..ded91621cb8 100644
--- a/app/assets/javascripts/feature_flags/components/strategy_parameters.vue
+++ b/app/assets/javascripts/feature_flags/components/strategy_parameters.vue
@@ -9,9 +9,9 @@ import {
import Default from './strategies/default.vue';
import FlexibleRollout from './strategies/flexible_rollout.vue';
+import GitlabUserList from './strategies/gitlab_user_list.vue';
import PercentRollout from './strategies/percent_rollout.vue';
import UsersWithId from './strategies/users_with_id.vue';
-import GitlabUserList from './strategies/gitlab_user_list.vue';
const STRATEGIES = Object.freeze({
[ROLLOUT_STRATEGY_ALL_USERS]: Default,
diff --git a/app/assets/javascripts/feature_flags/edit.js b/app/assets/javascripts/feature_flags/edit.js
index 05a9bbce654..010674592f8 100644
--- a/app/assets/javascripts/feature_flags/edit.js
+++ b/app/assets/javascripts/feature_flags/edit.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
-import createStore from './store/edit';
import EditFeatureFlag from './components/edit_feature_flag.vue';
+import createStore from './store/edit';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/feature_flags/new.js b/app/assets/javascripts/feature_flags/new.js
index 8e18213cc03..f763b12fedb 100644
--- a/app/assets/javascripts/feature_flags/new.js
+++ b/app/assets/javascripts/feature_flags/new.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
-import createStore from './store/new';
import NewFeatureFlag from './components/new_feature_flag.vue';
+import createStore from './store/new';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/feature_flags/store/edit/actions.js b/app/assets/javascripts/feature_flags/store/edit/actions.js
index c4515e07a00..72b17333832 100644
--- a/app/assets/javascripts/feature_flags/store/edit/actions.js
+++ b/app/assets/javascripts/feature_flags/store/edit/actions.js
@@ -1,10 +1,10 @@
-import * as types from './mutation_types';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import { NEW_VERSION_FLAG } from '../../constants';
import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers';
+import * as types from './mutation_types';
/**
* Handles the edition of a feature flag.
diff --git a/app/assets/javascripts/feature_flags/store/edit/index.js b/app/assets/javascripts/feature_flags/store/edit/index.js
index 65ea61c3025..16b8a5ae970 100644
--- a/app/assets/javascripts/feature_flags/store/edit/index.js
+++ b/app/assets/javascripts/feature_flags/store/edit/index.js
@@ -1,8 +1,8 @@
import Vuex from 'vuex';
import userLists from '../gitlab_user_list';
-import state from './state';
import * as actions from './actions';
import mutations from './mutations';
+import state from './state';
export default (data) =>
new Vuex.Store({
diff --git a/app/assets/javascripts/feature_flags/store/edit/mutations.js b/app/assets/javascripts/feature_flags/store/edit/mutations.js
index e60dbaf4a34..0a610f4b395 100644
--- a/app/assets/javascripts/feature_flags/store/edit/mutations.js
+++ b/app/assets/javascripts/feature_flags/store/edit/mutations.js
@@ -1,6 +1,6 @@
-import * as types from './mutation_types';
-import { mapToScopesViewModel, mapStrategiesToViewModel } from '../helpers';
import { LEGACY_FLAG } from '../../constants';
+import { mapToScopesViewModel, mapStrategiesToViewModel } from '../helpers';
+import * as types from './mutation_types';
export default {
[types.REQUEST_FEATURE_FLAG](state) {
diff --git a/app/assets/javascripts/feature_flags/store/gitlab_user_list/index.js b/app/assets/javascripts/feature_flags/store/gitlab_user_list/index.js
index 5f2726770d5..8a38f068609 100644
--- a/app/assets/javascripts/feature_flags/store/gitlab_user_list/index.js
+++ b/app/assets/javascripts/feature_flags/store/gitlab_user_list/index.js
@@ -1,7 +1,7 @@
-import state from './state';
-import mutations from './mutations';
import * as actions from './actions';
import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
export default (data) => ({
state: state(data),
diff --git a/app/assets/javascripts/feature_flags/store/gitlab_user_list/mutations.js b/app/assets/javascripts/feature_flags/store/gitlab_user_list/mutations.js
index bd7c6f68009..31e67a47541 100644
--- a/app/assets/javascripts/feature_flags/store/gitlab_user_list/mutations.js
+++ b/app/assets/javascripts/feature_flags/store/gitlab_user_list/mutations.js
@@ -1,5 +1,5 @@
-import statuses from './status';
import * as types from './mutation_types';
+import statuses from './status';
export default {
[types.FETCH_USER_LISTS](state) {
diff --git a/app/assets/javascripts/feature_flags/store/index/actions.js b/app/assets/javascripts/feature_flags/store/index/actions.js
index 6b6b3d55e16..4372c280f39 100644
--- a/app/assets/javascripts/feature_flags/store/index/actions.js
+++ b/app/assets/javascripts/feature_flags/store/index/actions.js
@@ -1,6 +1,6 @@
import Api from '~/api';
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
+import * as types from './mutation_types';
export const setFeatureFlagsOptions = ({ commit }, options) =>
commit(types.SET_FEATURE_FLAGS_OPTIONS, options);
diff --git a/app/assets/javascripts/feature_flags/store/index/index.js b/app/assets/javascripts/feature_flags/store/index/index.js
index 76495a33232..96ccb35fa21 100644
--- a/app/assets/javascripts/feature_flags/store/index/index.js
+++ b/app/assets/javascripts/feature_flags/store/index/index.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
-import state from './state';
import * as actions from './actions';
import mutations from './mutations';
+import state from './state';
export default (data) =>
new Vuex.Store({
diff --git a/app/assets/javascripts/feature_flags/store/index/mutations.js b/app/assets/javascripts/feature_flags/store/index/mutations.js
index 910b2ec42d4..25eb7da1c72 100644
--- a/app/assets/javascripts/feature_flags/store/index/mutations.js
+++ b/app/assets/javascripts/feature_flags/store/index/mutations.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
-import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../constants';
import { mapToScopesViewModel } from '../helpers';
+import * as types from './mutation_types';
const mapFlag = (flag) => ({ ...flag, scopes: mapToScopesViewModel(flag.scopes || []) });
diff --git a/app/assets/javascripts/feature_flags/store/new/actions.js b/app/assets/javascripts/feature_flags/store/new/actions.js
index 6d595603819..d0a1c77a69e 100644
--- a/app/assets/javascripts/feature_flags/store/new/actions.js
+++ b/app/assets/javascripts/feature_flags/store/new/actions.js
@@ -1,8 +1,8 @@
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { NEW_VERSION_FLAG } from '../../constants';
import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers';
+import * as types from './mutation_types';
/**
* Handles the creation of a new feature flag.
diff --git a/app/assets/javascripts/feature_flags/store/new/index.js b/app/assets/javascripts/feature_flags/store/new/index.js
index 65ea61c3025..16b8a5ae970 100644
--- a/app/assets/javascripts/feature_flags/store/new/index.js
+++ b/app/assets/javascripts/feature_flags/store/new/index.js
@@ -1,8 +1,8 @@
import Vuex from 'vuex';
import userLists from '../gitlab_user_list';
-import state from './state';
import * as actions from './actions';
import mutations from './mutations';
+import state from './state';
export default (data) =>
new Vuex.Store({
diff --git a/app/assets/javascripts/feature_highlight/constants.js b/app/assets/javascripts/feature_highlight/constants.js
new file mode 100644
index 00000000000..3e4cd11f7d5
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/constants.js
@@ -0,0 +1 @@
+export const POPOVER_TARGET_ID = 'feature-highlight-trigger';
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
deleted file mode 100644
index 2da9aadd2b1..00000000000
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import $ from 'jquery';
-import { getSelector, inserted } from './feature_highlight_helper';
-import { togglePopover, mouseenter, debouncedMouseleave } from '../shared/popover';
-
-export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
- const $selector = $(getSelector(id));
- const $parent = $selector.parent();
- const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
- const hideOnScroll = togglePopover.bind($selector, false);
-
- $selector
- // Set up popover
- .data('content', $popoverContent.prop('outerHTML'))
- .popover({
- html: true,
- // Override the existing template to add custom CSS classes
- template: `
- <div class="popover feature-highlight-popover" role="tooltip">
- <div class="arrow"></div>
- <div class="popover-body"></div>
- </div>
- `,
- })
- .on('mouseenter', mouseenter)
- .on('mouseleave', debouncedMouseleave(debounceTimeout))
- .on('inserted.bs.popover', inserted)
- .on('show.bs.popover', () => {
- window.addEventListener('scroll', hideOnScroll, { once: true });
- })
- // Display feature highlight
- .removeAttr('disabled');
-}
-
-const getPriority = (e) => parseInt(e.dataset.highlightPriority, 10) || 0;
-
-export function findHighestPriorityFeature() {
- let priorityFeature;
-
- const sortedFeatureEls = [].slice
- .call(document.querySelectorAll('.js-feature-highlight'))
- .sort((a, b) => getPriority(b) - getPriority(a));
-
- const [priorityFeatureEl] = sortedFeatureEls;
- if (priorityFeatureEl) {
- priorityFeature = priorityFeatureEl.dataset.highlight;
- }
-
- return priorityFeature;
-}
-
-export function highlightFeatures() {
- const priorityFeature = findHighestPriorityFeature();
-
- if (priorityFeature) {
- setupFeatureHighlightPopover(priorityFeature);
- }
-
- return priorityFeature;
-}
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
index fabc905d756..7b4bed69fb8 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -1,15 +1,12 @@
-import $ from 'jquery';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
-import { deprecatedCreateFlash as Flash } from '../flash';
-import LazyLoader from '../lazy_loader';
-import { togglePopover } from '../shared/popover';
export const getSelector = (highlightId) => `.js-feature-highlight[data-highlight=${highlightId}]`;
-export function dismiss(highlightId) {
- axios
- .post(this.attr('data-dismiss-endpoint'), {
+export function dismiss(endpoint, highlightId) {
+ return axios
+ .post(endpoint, {
feature_name: highlightId,
})
.catch(() =>
@@ -19,21 +16,4 @@ export function dismiss(highlightId) {
),
),
);
-
- togglePopover.call(this, false);
- this.hide();
-}
-
-export function inserted() {
- const popoverId = this.getAttribute('aria-describedby');
- const highlightId = this.dataset.highlight;
- const $popover = $(this);
- const dismissWrapper = dismiss.bind($popover, highlightId);
-
- $(`#${popoverId} .dismiss-feature-highlight`).on('click', dismissWrapper);
-
- const lazyImg = $(`#${popoverId} .feature-highlight-illustration`)[0];
- if (lazyImg) {
- LazyLoader.loadImage(lazyImg);
- }
}
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js
deleted file mode 100644
index c5553f0243f..00000000000
--- a/app/assets/javascripts/feature_highlight/feature_highlight_options.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { highlightFeatures } from './feature_highlight';
-
-export default function domContentLoaded() {
- if (bp.getBreakpointSize() === 'xl') {
- highlightFeatures();
- return true;
- }
- return false;
-}
-
-document.addEventListener('DOMContentLoaded', domContentLoaded);
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue b/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
new file mode 100644
index 00000000000..2fd92a1bb11
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
@@ -0,0 +1,101 @@
+<script>
+import clusterPopover from '@gitlab/svgs/dist/illustrations/cluster_popover.svg';
+import {
+ GlPopover,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
+import { __ } from '~/locale';
+import { POPOVER_TARGET_ID } from './constants';
+import { dismiss } from './feature_highlight_helper';
+
+export default {
+ components: {
+ GlPopover,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
+ highlightId: {
+ type: String,
+ required: true,
+ },
+ dismissEndpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ dismissed: false,
+ triggerHidden: false,
+ };
+ },
+ methods: {
+ dismiss() {
+ dismiss(this.dismissEndpoint, this.highlightId);
+ this.$refs.popover.$emit('close');
+ this.dismissed = true;
+ },
+ hideTrigger() {
+ if (this.dismissed) {
+ this.triggerHidden = true;
+ }
+ },
+ },
+ clusterPopover,
+ targetId: POPOVER_TARGET_ID,
+ i18n: {
+ highlightMessage: __('Allows you to add and manage Kubernetes clusters.'),
+ autoDevopsProTipMessage: __(
+ 'Protip: %{linkStart}Auto DevOps%{linkEnd} uses Kubernetes clusters to deploy your code!',
+ ),
+ dismissButtonLabel: __('Got it!'),
+ },
+};
+</script>
+<template>
+ <div class="gl-ml-3">
+ <span v-if="!triggerHidden" :id="$options.targetId" class="feature-highlight"></span>
+ <gl-popover
+ ref="popover"
+ :target="$options.targetId"
+ :css-classes="['feature-highlight-popover']"
+ triggers="hover"
+ container="body"
+ placement="right"
+ boundary="viewport"
+ @hidden="hideTrigger"
+ >
+ <span
+ v-safe-html="$options.clusterPopover"
+ class="feature-highlight-illustration gl-display-flex gl-justify-content-center gl-py-4 gl-w-full"
+ ></span>
+ <div class="gl-px-4 gl-py-5">
+ <p>
+ {{ $options.i18n.highlightMessage }}
+ </p>
+ <p>
+ <gl-sprintf :message="$options.i18n.autoDevopsProTipMessage">
+ <template #link="{ content }">
+ <gl-link class="gl-font-sm" :href="autoDevopsHelpPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-button size="small" icon="thumb-up" variant="confirm" @click="dismiss">
+ {{ $options.i18n.dismissButtonLabel }}
+ </gl-button>
+ </div>
+ </gl-popover>
+ </div>
+</template>
diff --git a/app/assets/javascripts/feature_highlight/index.js b/app/assets/javascripts/feature_highlight/index.js
new file mode 100644
index 00000000000..3a8b211b3c5
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/index.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+
+const init = async () => {
+ const el = document.querySelector('.js-feature-highlight');
+
+ if (!el) {
+ return null;
+ }
+
+ const { autoDevopsHelpPath, highlight: highlightId, dismissEndpoint } = el.dataset;
+ const { default: FeatureHighlight } = await import(
+ /* webpackChunkName: 'feature_highlight' */ './feature_highlight_popover.vue'
+ );
+
+ return new Vue({
+ el,
+ render: (h) =>
+ h(FeatureHighlight, {
+ props: {
+ autoDevopsHelpPath,
+ highlightId,
+ dismissEndpoint,
+ },
+ }),
+ });
+};
+
+export default init;
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 588bd534224..409b4ccbcfa 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -1,12 +1,12 @@
+import { mergeUrlParams } from '../lib/utils/url_utility';
+import DropdownAjaxFilter from './dropdown_ajax_filter';
+import DropdownEmoji from './dropdown_emoji';
import DropdownHint from './dropdown_hint';
-import DropdownUser from './dropdown_user';
import DropdownNonUser from './dropdown_non_user';
-import DropdownEmoji from './dropdown_emoji';
-import NullDropdown from './null_dropdown';
-import DropdownAjaxFilter from './dropdown_ajax_filter';
import DropdownOperator from './dropdown_operator';
+import DropdownUser from './dropdown_user';
import DropdownUtils from './dropdown_utils';
-import { mergeUrlParams } from '../lib/utils/url_utility';
+import NullDropdown from './null_dropdown';
export default class AvailableDropdownMappings {
constructor({
diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
index 2c0c3024d38..e317700b09b 100644
--- a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
+++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
@@ -1,9 +1,9 @@
-import { deprecatedCreateFlash as createFlash } from '../flash';
+import { __ } from '~/locale';
import AjaxFilter from '../droplab/plugins/ajax_filter';
-import FilteredSearchDropdown from './filtered_search_dropdown';
+import { deprecatedCreateFlash as createFlash } from '../flash';
import DropdownUtils from './dropdown_utils';
+import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
-import { __ } from '~/locale';
export default class DropdownAjaxFilter extends FilteredSearchDropdown {
constructor(options = {}) {
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index 001030b5f5f..a22430833a3 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -1,9 +1,9 @@
-import { deprecatedCreateFlash as Flash } from '../flash';
+import { __ } from '~/locale';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
-import FilteredSearchDropdown from './filtered_search_dropdown';
+import { deprecatedCreateFlash as Flash } from '../flash';
import DropdownUtils from './dropdown_utils';
-import { __ } from '~/locale';
+import FilteredSearchDropdown from './filtered_search_dropdown';
export default class DropdownEmoji extends FilteredSearchDropdown {
constructor(options = {}) {
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 1180f8683a1..47f350dc6a2 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -1,9 +1,9 @@
import Filter from '~/droplab/plugins/filter';
-import FilteredSearchDropdown from './filtered_search_dropdown';
+import { __ } from '~/locale';
import DropdownUtils from './dropdown_utils';
+import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
-import { __ } from '~/locale';
export default class DropdownHint extends FilteredSearchDropdown {
constructor(options = {}) {
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index 11261debeda..4df1120f169 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -1,9 +1,9 @@
-import { deprecatedCreateFlash as Flash } from '../flash';
+import { __ } from '~/locale';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
-import FilteredSearchDropdown from './filtered_search_dropdown';
+import { deprecatedCreateFlash as Flash } from '../flash';
import DropdownUtils from './dropdown_utils';
-import { __ } from '~/locale';
+import FilteredSearchDropdown from './filtered_search_dropdown';
export default class DropdownNonUser extends FilteredSearchDropdown {
constructor(options = {}) {
diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js
index 8fee3385de1..0da8cd0ad83 100644
--- a/app/assets/javascripts/filtered_search/dropdown_operator.js
+++ b/app/assets/javascripts/filtered_search/dropdown_operator.js
@@ -1,7 +1,7 @@
import Filter from '~/droplab/plugins/filter';
import { __ } from '~/locale';
-import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
+import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 22c98f360ed..c98d1f8e064 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -1,7 +1,7 @@
import { last } from 'lodash';
import FilteredSearchContainer from './container';
-import FilteredSearchTokenizer from './filtered_search_tokenizer';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
+import FilteredSearchTokenizer from './filtered_search_tokenizer';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class DropdownUtils {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 7434cc4c5d1..fcc7caa9ff2 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -1,7 +1,7 @@
+import { FILTER_TYPE } from './constants';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
-import { FILTER_TYPE } from './constants';
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
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 3c630c26bc7..ebaa3ef98b1 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -1,11 +1,11 @@
import { last } from 'lodash';
import AvailableDropdownMappings from 'ee_else_ce/filtered_search/available_dropdown_mappings';
import DropLab from '~/droplab/drop_lab';
+import { DROPDOWN_TYPE } from './constants';
import FilteredSearchContainer from './container';
-import FilteredSearchTokenKeys from './filtered_search_token_keys';
import DropdownUtils from './dropdown_utils';
+import FilteredSearchTokenKeys from './filtered_search_token_keys';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
-import { DROPDOWN_TYPE } from './constants';
export default class FilteredSearchDropdownManager {
constructor({
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 11b2eb839ce..69d19074cd0 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,19 +1,7 @@
import { last } from 'lodash';
import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
-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 { deprecatedCreateFlash as Flash } from '../flash';
-import FilteredSearchContainer from './container';
-import RecentSearchesRoot from './recent_searches_root';
-import RecentSearchesStore from './stores/recent_searches_store';
-import RecentSearchesService from './services/recent_searches_service';
-import eventHub from './event_hub';
-import { addClassIfElementExists } from '../lib/utils/dom_utils';
-import FilteredSearchTokenizer from './filtered_search_tokenizer';
-import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
-import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
-import DropdownUtils from './dropdown_utils';
+import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
import {
ENTER_KEY_CODE,
BACKSPACE_KEY_CODE,
@@ -22,6 +10,18 @@ import {
DOWN_KEY_CODE,
} from '~/lib/utils/keycodes';
import { __ } from '~/locale';
+import { deprecatedCreateFlash as Flash } from '../flash';
+import { addClassIfElementExists } from '../lib/utils/dom_utils';
+import { visitUrl } from '../lib/utils/url_utility';
+import FilteredSearchContainer from './container';
+import DropdownUtils from './dropdown_utils';
+import eventHub from './event_hub';
+import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
+import FilteredSearchTokenizer from './filtered_search_tokenizer';
+import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
+import RecentSearchesRoot from './recent_searches_root';
+import RecentSearchesService from './services/recent_searches_service';
+import RecentSearchesStore from './stores/recent_searches_store';
export default class FilteredSearchManager {
constructor({
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index 4e594dfa910..eec4db41b0a 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,6 +1,6 @@
-import VisualTokenValue from './visual_token_value';
import { objectToQueryString, spriteIcon } from '~/lib/utils/common_utils';
import FilteredSearchContainer from './container';
+import VisualTokenValue from './visual_token_value';
export default class FilteredSearchVisualTokens {
static permissibleOperatorValues = ['=', '!='];
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 46867b184c8..2c58506985a 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
@@ -1,6 +1,6 @@
import { flattenDeep } from 'lodash';
-import FilteredSearchTokenKeys from './filtered_search_token_keys';
import { __ } from '~/locale';
+import FilteredSearchTokenKeys from './filtered_search_token_keys';
export const tokenKeys = [
{
diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js
index 6c8e77a7fe5..1182cb34210 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_root.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_root.js
@@ -28,19 +28,18 @@ class RecentSearchesRoot {
const { state } = this.store;
this.vm = new Vue({
el: this.wrapperElement,
- components: {
- RecentSearchesDropdownContent,
- },
data() {
return state;
},
- template: `
- <recent-searches-dropdown-content
- :items="recentSearches"
- :is-local-storage-available="isLocalStorageAvailable"
- :allowed-keys="allowedKeys"
- />
- `,
+ render(h) {
+ return h(RecentSearchesDropdownContent, {
+ props: {
+ items: this.recentSearches,
+ isLocalStorageAvailable: this.isLocalStorageAvailable,
+ allowedKeys: this.allowedKeys,
+ },
+ });
+ },
});
}
diff --git a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
index 54d49821d92..446a0e5eb24 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
@@ -3,4 +3,6 @@ export default {
merge_requests: 'merge-request-recent-searches',
group_members: 'group-members-recent-searches',
group_invited_members: 'group-invited-members-recent-searches',
+ project_members: 'project-members-recent-searches',
+ project_group_links: 'project-group-links-recent-searches',
};
diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service.js b/app/assets/javascripts/filtered_search/services/recent_searches_service.js
index a056dea928d..56824977a43 100644
--- a/app/assets/javascripts/filtered_search/services/recent_searches_service.js
+++ b/app/assets/javascripts/filtered_search/services/recent_searches_service.js
@@ -1,5 +1,5 @@
-import RecentSearchesServiceError from './recent_searches_service_error';
import AccessorUtilities from '../../lib/utils/accessor';
+import RecentSearchesServiceError from './recent_searches_service_error';
class RecentSearchesService {
constructor(localStorageKey = 'issuable-recent-searches') {
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index 0d36126943b..7f4445ad4c7 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -1,13 +1,13 @@
import { escape } from 'lodash';
import { USER_TOKEN_TYPES } from 'ee_else_ce/filtered_search/constants';
+import * as Emoji from '~/emoji';
import FilteredSearchContainer from '~/filtered_search/container';
-import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
-import AjaxCache from '~/lib/utils/ajax_cache';
import DropdownUtils from '~/filtered_search/dropdown_utils';
+import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale';
-import * as Emoji from '~/emoji';
export default class VisualTokenValue {
constructor(tokenValue, tokenType, tokenOperator) {
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 68cc8645813..69f89aa3857 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -1,13 +1,13 @@
<script>
-import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import { mapState, mapActions, mapGetters } from 'vuex';
import AccessorUtilities from '~/lib/utils/accessor';
-import eventHub from '../event_hub';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
+import eventHub from '../event_hub';
import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
-import FrequentItemsSearchInput from './frequent_items_search_input.vue';
import FrequentItemsList from './frequent_items_list.vue';
import frequentItemsMixin from './frequent_items_mixin';
+import FrequentItemsSearchInput from './frequent_items_search_input.vue';
export default {
components: {
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
index 9d898d1a1a1..6feeb5f03ad 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
@@ -1,7 +1,7 @@
<script>
+import { sanitizeItem } from '../utils';
import FrequentItemsListItem from './frequent_items_list_item.vue';
import frequentItemsMixin from './frequent_items_mixin';
-import { sanitizeItem } from '../utils';
export default {
components: {
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 3260d768fd9..6f17e6a5282 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,10 +1,10 @@
<script>
/* eslint-disable vue/require-default-prop, vue/no-v-html */
import { mapState } from 'vuex';
-import Identicon from '~/vue_shared/components/identicon.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
+import Identicon from '~/vue_shared/components/identicon.vue';
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
index 8042e8c7bc9..b0972246e70 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
@@ -1,16 +1,15 @@
<script>
+import { GlSearchBoxByType } from '@gitlab/ui';
import { debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
-import { GlIcon } from '@gitlab/ui';
-import eventHub from '../event_hub';
-import frequentItemsMixin from './frequent_items_mixin';
import Tracking from '~/tracking';
+import frequentItemsMixin from './frequent_items_mixin';
const trackingMixin = Tracking.mixin();
export default {
components: {
- GlIcon,
+ GlSearchBoxByType,
},
mixins: [frequentItemsMixin, trackingMixin],
data() {
@@ -32,30 +31,17 @@ export default {
this.setSearchQuery(this.searchQuery);
}, 500),
},
- mounted() {
- eventHub.$on(`${this.namespace}-dropdownOpen`, this.setFocus);
- },
- beforeDestroy() {
- eventHub.$off(`${this.namespace}-dropdownOpen`, this.setFocus);
- },
methods: {
...mapActions(['setSearchQuery']),
- setFocus() {
- this.$refs.search.focus();
- },
},
};
</script>
<template>
<div class="search-input-container d-none d-sm-block">
- <input
- ref="search"
+ <gl-search-box-by-type
v-model="searchQuery"
:placeholder="translations.searchInputPlaceholder"
- type="search"
- class="form-control"
/>
- <gl-icon v-if="!searchQuery" name="search" class="search-icon" />
</div>
</template>
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
index cef8be37a40..eb8a404e8a5 100644
--- a/app/assets/javascripts/frequent_items/index.js
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import Vue from 'vue';
+import { createStore } from '~/frequent_items/store';
import Translate from '~/vue_shared/translate';
import eventHub from './event_hub';
-import { createStore } from '~/frequent_items/store';
Vue.use(Translate);
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
index f4156487625..90b454d1b42 100644
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -1,7 +1,7 @@
import AccessorUtilities from '~/lib/utils/accessor';
-import * as types from './mutation_types';
-import { getTopFrequentItems } from '../utils';
import { getGroups, getProjects } from '~/rest_api';
+import { getTopFrequentItems } from '../utils';
+import * as types from './mutation_types';
export const setNamespace = ({ commit }, namespace) => {
commit(types.SET_NAMESPACE, namespace);
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
index 63fe0ef20b0..88519d934cb 100644
--- a/app/assets/javascripts/frequent_items/utils.js
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -1,5 +1,5 @@
-import { take } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { take } from 'lodash';
import { sanitize } from '~/lib/dompurify';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index cf9ff87f25e..d209a971c39 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,19 +1,23 @@
import $ from 'jquery';
import '~/lib/utils/jquery_at_who';
import { escape, template } from 'lodash';
+import * as Emoji from '~/emoji';
+import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
import { isUserBusy } from '~/set_status_modal/utils';
-import glRegexp from './lib/utils/regexp';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
import AjaxCache from './lib/utils/ajax_cache';
-import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from './lib/utils/common_utils';
-import * as Emoji from '~/emoji';
+import glRegexp from './lib/utils/regexp';
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
+function createMemberSearchString(member) {
+ return `${member.name.replace(/ /g, '')} ${member.username}`;
+}
+
export function membersBeforeSave(members) {
return members.map((member) => {
const GROUP_TYPE = 'Group';
@@ -40,7 +44,7 @@ export function membersBeforeSave(members) {
username: member.username,
avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
title: sanitize(title),
- search: sanitize(`${member.username} ${member.name}`),
+ search: sanitize(createMemberSearchString(member)),
icon: avatarIcon,
availability: member?.availability,
};
@@ -186,59 +190,43 @@ class GfmAutoComplete {
}
setupEmoji($input) {
- const self = this;
- const { filter, ...defaults } = this.getDefaultCallbacks();
+ const fetchData = this.fetchData.bind(this);
// Emoji
$input.atwho({
at: ':',
- displayTpl(value) {
- let tmpl = GfmAutoComplete.Loading.template;
- if (value && value.name) {
- tmpl = GfmAutoComplete.Emoji.templateFunction(value.name);
- }
- return tmpl;
- },
+ displayTpl: GfmAutoComplete.Emoji.templateFunction,
insertTpl: GfmAutoComplete.Emoji.insertTemplateFunction,
skipSpecialCharacterTest: true,
data: GfmAutoComplete.defaultLoadingData,
callbacks: {
- ...defaults,
+ ...this.getDefaultCallbacks(),
matcher(flag, subtext) {
const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
const match = regexp.exec(subtext);
return match && match.length ? match[1] : null;
},
- filter(query, items, searchKey) {
- const filtered = filter.call(this, query, items, searchKey);
- if (query.length === 0 || GfmAutoComplete.isLoading(items)) {
- return filtered;
+ filter(query, items) {
+ if (GfmAutoComplete.isLoading(items)) {
+ fetchData(this.$inputor, this.at);
+ return items;
}
- // map from value to "<value> is <field> of <emoji>", arranged by emoji
- const emojis = {};
- filtered.forEach(({ name: value }) => {
- self.emojiLookup[value].forEach(({ emoji: { name }, kind }) => {
- let entry = emojis[name];
- if (!entry) {
- entry = {};
- emojis[name] = entry;
- }
- if (!(kind in entry) || value.localeCompare(entry[kind]) < 0) {
- entry[kind] = value;
- }
- });
- });
+ return GfmAutoComplete.Emoji.filter(query);
+ },
+ sorter(query, items) {
+ this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
+ if (GfmAutoComplete.isLoading(items)) {
+ this.setting.highlightFirst = false;
+ return items;
+ }
- // collate results to list, prefering name > unicode > alias > description
- const results = [];
- Object.values(emojis).forEach(({ name, unicode, alias, description }) => {
- results.push(name || unicode || alias || description);
- });
+ if (query.length === 0) {
+ return items;
+ }
- // return to the form atwho wants
- return results.map((name) => ({ name }));
+ return GfmAutoComplete.Emoji.sorter(items);
},
},
});
@@ -298,9 +286,7 @@ class GfmAutoComplete {
// Cache assignees list for easier filtering later
assignees =
- SidebarMediator.singleton?.store?.assignees?.map(
- (assignee) => `${assignee.username} ${assignee.name}`,
- ) || [];
+ SidebarMediator.singleton?.store?.assignees?.map(createMemberSearchString) || [];
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
return match && match.length ? match[1] : null;
@@ -672,32 +658,7 @@ class GfmAutoComplete {
async loadEmojiData($input, at) {
await Emoji.initEmojiMap();
- // All the emoji
- const emojis = Emoji.getAllEmoji();
-
- // Add all of the fields to atwho's database
- this.loadData($input, at, [
- ...Object.keys(emojis), // Names
- ...Object.values(emojis).flatMap(({ aliases }) => aliases), // Aliases
- ...Object.values(emojis).map(({ e }) => e), // Unicode values
- ...Object.values(emojis).map(({ d }) => d), // Descriptions
- ]);
-
- // Construct a lookup that can correlate a value to "<value> is the <field> of <emoji>"
- const lookup = {};
- const add = (key, kind, emoji) => {
- if (!(key in lookup)) {
- lookup[key] = [];
- }
- lookup[key].push({ kind, emoji });
- };
- Object.values(emojis).forEach((emoji) => {
- add(emoji.name, 'name', emoji);
- add(emoji.d, 'description', emoji);
- add(emoji.e, 'unicode', emoji);
- emoji.aliases.forEach((a) => add(a, 'alias', emoji));
- });
- this.emojiLookup = lookup;
+ this.loadData($input, at, ['loaded']);
GfmAutoComplete.glEmojiTag = Emoji.glEmojiTag;
}
@@ -770,36 +731,38 @@ GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities'];
GfmAutoComplete.isTypeWithBackendFiltering = (type) =>
GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[type]);
-function findEmoji(name) {
- return Emoji.searchEmoji(name, { match: 'contains', raw: true }).sort((a, b) => {
- if (a.index !== b.index) {
- return a.index - b.index;
- }
- return a.field.localeCompare(b.field);
- });
-}
-
// Emoji
GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = {
insertTemplateFunction(value) {
- const results = findEmoji(value.name);
- if (results.length) {
- return `:${results[0].emoji.name}:`;
- }
- return `:${value.name}:`;
+ return `:${value.emoji.name}:`;
},
- templateFunction(name) {
- // glEmojiTag helper is loaded on-demand in fetchData()
- if (!GfmAutoComplete.glEmojiTag) return `<li>${name}</li>`;
+ templateFunction(item) {
+ if (GfmAutoComplete.isLoading(item)) {
+ return GfmAutoComplete.Loading.template;
+ }
- const results = findEmoji(name);
- if (!results.length) {
- return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
+ const escapedFieldValue = escape(item.fieldValue);
+ if (!GfmAutoComplete.glEmojiTag) {
+ return `<li>${escapedFieldValue}</li>`;
}
- const { field, emoji } = results[0];
- return `<li>${field} ${GfmAutoComplete.glEmojiTag(emoji.name)}</li>`;
+ return `<li>${escapedFieldValue} ${GfmAutoComplete.glEmojiTag(item.emoji.name)}</li>`;
+ },
+ filter(query) {
+ if (query.length === 0) {
+ return Object.values(Emoji.getAllEmoji())
+ .map((emoji) => ({
+ emoji,
+ fieldValue: emoji.name,
+ }))
+ .slice(0, 20);
+ }
+
+ return Emoji.searchEmoji(query);
+ },
+ sorter(items) {
+ return Emoji.sortEmoji(items);
},
};
// Team Members
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 3e777c2dc09..3a04779e48d 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,9 +1,9 @@
-import $ from 'jquery';
import autosize from 'autosize';
+import $ from 'jquery';
import GfmAutoComplete, { defaultAutocompleteConfig } from 'ee_else_ce/gfm_auto_complete';
+import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
import dropzoneInput from './dropzone_input';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
-import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
export default class GLForm {
/**
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index 3a8ae56bb8f..cde2cd6d6ab 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
export default class GpgBadges {
diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
index 7a991ac2455..7b029c6cf54 100644
--- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
+++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
@@ -54,9 +54,9 @@ export default {
<template>
<section id="grafana" class="settings no-animate js-grafana-integration">
<div class="settings-header">
- <h3 class="js-section-header h4">
+ <h4 class="js-section-header">
{{ s__('GrafanaIntegration|Grafana authentication') }}
- </h3>
+ </h4>
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{ s__('GrafanaIntegration|Embed Grafana charts in GitLab issues.') }}
diff --git a/app/assets/javascripts/grafana_integration/index.js b/app/assets/javascripts/grafana_integration/index.js
index a93edab4388..208a92c97c7 100644
--- a/app/assets/javascripts/grafana_integration/index.js
+++ b/app/assets/javascripts/grafana_integration/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from './store';
import GrafanaIntegration from './components/grafana_integration.vue';
+import store from './store';
export default () => {
const el = document.querySelector('.js-grafana-integration');
diff --git a/app/assets/javascripts/grafana_integration/store/actions.js b/app/assets/javascripts/grafana_integration/store/actions.js
index 436f92eae84..7c5d4695731 100644
--- a/app/assets/javascripts/grafana_integration/store/actions.js
+++ b/app/assets/javascripts/grafana_integration/store/actions.js
@@ -1,7 +1,7 @@
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import * as mutationTypes from './mutation_types';
export const setGrafanaUrl = ({ commit }, url) => commit(mutationTypes.SET_GRAFANA_URL, url);
diff --git a/app/assets/javascripts/grafana_integration/store/index.js b/app/assets/javascripts/grafana_integration/store/index.js
index e3dcfd31a83..a11bd8089fd 100644
--- a/app/assets/javascripts/grafana_integration/store/index.js
+++ b/app/assets/javascripts/grafana_integration/store/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
+import createState from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql
index 74b425717a0..801311301ac 100644
--- a/app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql
@@ -1,4 +1,4 @@
-#import "~/graphql_shared/fragments/author.fragment.graphql"
+#import "./author.fragment.graphql"
fragment AlertNote on Note {
id
diff --git a/app/assets/javascripts/graphql_shared/mutations/update_alert_status.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql
index 42dc388c9d1..ba1e607bc10 100644
--- a/app/assets/javascripts/graphql_shared/mutations/update_alert_status.mutation.graphql
+++ b/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/graphql_shared/fragments/alert_note.fragment.graphql"
+#import "../fragments/alert_note.fragment.graphql"
mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
diff --git a/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql b/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql
index e94758ef60e..7a676e67f1b 100644
--- a/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql
@@ -10,6 +10,7 @@ query getAlerts(
$nextPageCursor: String = ""
$searchTerm: String = ""
$assigneeUsername: String = ""
+ $domain: AlertManagementDomainFilter = operations
) {
project(fullPath: $projectPath) {
alertManagementAlerts(
@@ -21,6 +22,7 @@ query getAlerts(
last: $lastPageSize
after: $nextPageCursor
before: $prevPageCursor
+ domain: $domain
) {
nodes {
...AlertListItem
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index 6878635b288..39c8a88d485 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,7 +1,7 @@
-import { slugify } from './lib/utils/text_utility';
-import fetchGroupPathAvailability from '~/pages/groups/new/fetch_group_path_availability';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
+import fetchGroupPathAvailability from '~/pages/groups/new/fetch_group_path_availability';
+import { slugify } from './lib/utils/text_utility';
export default class Group {
constructor() {
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index bfaa54080bd..257f5ac9658 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { __ } from '~/locale';
-import axios from './lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from './flash';
import { fixTitle, hide } from '~/tooltips';
+import { deprecatedCreateFlash as flash } from './flash';
+import axios from './lib/utils/axios_utils';
const tooltipTitles = {
group: __('Unsubscribe at group level'),
diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
index d6ed08a9407..a1d706f0f66 100644
--- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue
+++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
@@ -1,8 +1,8 @@
<script>
import { GlToggle, GlLoadingIcon, GlTooltip, GlAlert } from '@gitlab/ui';
import { debounce } from 'lodash';
-import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
import {
DEBOUNCE_TOGGLE_DELAY,
ERROR_MESSAGE,
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 4f26bab8bd3..9d2c7cfe581 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -2,13 +2,13 @@
/* global Flash */
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
-import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
+import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { __, s__, sprintf } from '~/locale';
-import eventHub from '../event_hub';
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
+import eventHub from '../event_hub';
import groupsComponent from './groups.vue';
export default {
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index d65ad974c73..d41fa0b2410 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -3,16 +3,14 @@
import { GlLoadingIcon, GlBadge, GlTooltipDirective } from '@gitlab/ui';
import { visitUrl } from '../../lib/utils/url_utility';
import identicon from '../../vue_shared/components/identicon.vue';
-import eventHub from '../event_hub';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '../constants';
+import eventHub from '../event_hub';
+import itemActions from './item_actions.vue';
import itemCaret from './item_caret.vue';
-import itemTypeIcon from './item_type_icon.vue';
import itemStats from './item_stats.vue';
import itemStatsValue from './item_stats_value.vue';
-import itemActions from './item_actions.vue';
-
-import { showLearnGitLabGroupItemPopover } from '~/onboarding_issues';
+import itemTypeIcon from './item_type_icon.vue';
export default {
directives: {
@@ -78,11 +76,6 @@ export default {
return this.group.microdata || {};
},
},
- mounted() {
- if (this.group.name === 'Learn GitLab') {
- showLearnGitLabGroupItemPopover(this.group.id);
- }
- },
methods: {
onClickRowGroup(e) {
const NO_EXPAND_CLS = 'no-expand';
@@ -179,7 +172,12 @@ export default {
<div
class="metadata align-items-md-center d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between"
>
- <item-actions v-if="isGroup" :group="group" :parent-group="parentGroup" />
+ <item-actions
+ v-if="isGroup"
+ :group="group"
+ :parent-group="parentGroup"
+ :action="action"
+ />
<item-stats :item="group" class="group-stats gl-mt-2 d-none d-md-flex" />
</div>
</div>
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index c7713cbfafc..d407fdd2b90 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -1,7 +1,7 @@
<script>
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
-import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils';
+import eventHub from '../event_hub';
export default {
components: {
diff --git a/app/assets/javascripts/groups/components/invite_members_banner.vue b/app/assets/javascripts/groups/components/invite_members_banner.vue
index da7adab1d86..81c5e3ce85d 100644
--- a/app/assets/javascripts/groups/components/invite_members_banner.vue
+++ b/app/assets/javascripts/groups/components/invite_members_banner.vue
@@ -1,7 +1,7 @@
<script>
import { GlBanner } from '@gitlab/ui';
-import { s__ } from '~/locale';
import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index ff52f5ef51c..df751a3f37e 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
-import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
+import eventHub from '../event_hub';
export default {
components: {
diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js
index c33ad8b6ecb..cedf16cd7f1 100644
--- a/app/assets/javascripts/groups/groups_filterable_list.js
+++ b/app/assets/javascripts/groups/groups_filterable_list.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import FilterableList from '~/filterable_list';
-import eventHub from './event_hub';
import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils';
+import eventHub from './event_hub';
export default class GroupFilterableList extends FilterableList {
constructor({
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index e11c3aaf984..c34810954a3 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -1,16 +1,16 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import UserCallout from '~/user_callout';
+import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import UserCallout from '~/user_callout';
import Translate from '../vue_shared/translate';
-import GroupFilterableList from './groups_filterable_list';
-import GroupsStore from './store/groups_store';
-import GroupsService from './service/groups_service';
import groupsApp from './components/app.vue';
import groupFolderComponent from './components/group_folder.vue';
import groupItemComponent from './components/group_item.vue';
import { GROUPS_LIST_HOLDER_CLASS, CONTENT_LIST_CLASS } from './constants';
+import GroupFilterableList from './groups_filterable_list';
+import GroupsService from './service/groups_service';
+import GroupsStore from './store/groups_store';
Vue.use(Translate);
@@ -33,8 +33,8 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
dataEl = containerEl.querySelector(CONTENT_LIST_CLASS);
}
- Vue.component('group-folder', groupFolderComponent);
- Vue.component('group-item', groupItemComponent);
+ Vue.component('GroupFolder', groupFolderComponent);
+ Vue.component('GroupItem', groupItemComponent);
Vue.use(GlToast);
diff --git a/app/assets/javascripts/groups/members/constants.js b/app/assets/javascripts/groups/members/constants.js
index 6d71b666d7a..3315712891d 100644
--- a/app/assets/javascripts/groups/members/constants.js
+++ b/app/assets/javascripts/groups/members/constants.js
@@ -1,5 +1 @@
export const GROUP_MEMBER_BASE_PROPERTY_NAME = 'group_member';
-export const GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME = 'access_level';
-
-export const GROUP_LINK_BASE_PROPERTY_NAME = 'group_link';
-export const GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME = 'group_access';
diff --git a/app/assets/javascripts/groups/members/utils.js b/app/assets/javascripts/groups/members/utils.js
index 4fcf348b69f..dc4eb7f4422 100644
--- a/app/assets/javascripts/groups/members/utils.js
+++ b/app/assets/javascripts/groups/members/utils.js
@@ -1,45 +1,8 @@
-import { isUndefined } from 'lodash';
-import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
-import {
- GROUP_MEMBER_BASE_PROPERTY_NAME,
- GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
- GROUP_LINK_BASE_PROPERTY_NAME,
- GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
-} from './constants';
-
-export const parseDataAttributes = (el) => {
- const { members, groupId, memberPath, canManageMembers } = el.dataset;
-
- return {
- members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
- sourceId: parseInt(groupId, 10),
- memberPath,
- canManageMembers: parseBoolean(canManageMembers),
- };
-};
-
-const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({
- accessLevel,
- ...otherProperties
-}) => {
- const accessLevelProperty = !isUndefined(accessLevel)
- ? { [accessLevelPropertyName]: accessLevel }
- : {};
+import { MEMBER_ACCESS_LEVEL_PROPERTY_NAME } from '~/members/constants';
+import { baseRequestFormatter } from '~/members/utils';
+import { GROUP_MEMBER_BASE_PROPERTY_NAME } from './constants';
- return {
- [basePropertyName]: {
- ...accessLevelProperty,
- ...otherProperties,
- },
- };
-};
-
-export const memberRequestFormatter = baseRequestFormatter(
+export const groupMemberRequestFormatter = baseRequestFormatter(
GROUP_MEMBER_BASE_PROPERTY_NAME,
- GROUP_MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
-);
-
-export const groupLinkRequestFormatter = baseRequestFormatter(
- GROUP_LINK_BASE_PROPERTY_NAME,
- GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
+ MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
);
diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js
index 59cc779d2ae..d6343f698c0 100644
--- a/app/assets/javascripts/groups/transfer_dropdown.js
+++ b/app/assets/javascripts/groups/transfer_dropdown.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { __ } from '~/locale';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __ } from '~/locale';
export default class TransferDropdown {
constructor() {
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index c65fff432d0..da2890f91fc 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import { escape } from 'lodash';
-import axios from './lib/utils/axios_utils';
+import { __ } from '~/locale';
import Api from './api';
+import axios from './lib/utils/axios_utils';
import { normalizeHeaders } from './lib/utils/common_utils';
-import { __ } from '~/locale';
import { loadCSSFile } from './lib/utils/css_utils';
const fetchGroups = (params) => {
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 9f9708bf879..22c648a76a7 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
+import Translate from '~/vue_shared/translate';
/**
* Updates todo counter when todos are toggled.
@@ -106,7 +106,5 @@ export function initNavUserDropdownTracking() {
}
}
-document.addEventListener('DOMContentLoaded', () => {
- requestIdleCallback(initStatusTriggers);
- requestIdleCallback(initNavUserDropdownTracking);
-});
+requestIdleCallback(initStatusTriggers);
+requestIdleCallback(initNavUserDropdownTracking);
diff --git a/app/assets/javascripts/helpers/avatar_helper.js b/app/assets/javascripts/helpers/avatar_helper.js
index 4f04a1b8c16..35b09e9b027 100644
--- a/app/assets/javascripts/helpers/avatar_helper.js
+++ b/app/assets/javascripts/helpers/avatar_helper.js
@@ -1,6 +1,6 @@
import { escape } from 'lodash';
-import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
export const DEFAULT_SIZE_CLASS = 's40';
export const IDENTICON_BG_COUNT = 7;
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 644808cb83a..c71d911adfb 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -1,6 +1,7 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { leftSidebarViews } from '../constants';
export default {
@@ -11,7 +12,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
computed: {
- ...mapState(['currentActivityView']),
+ ...mapState(['currentActivityView', 'stagedFiles']),
},
methods: {
...mapActions(['updateActivityBarView']),
@@ -20,7 +21,7 @@ export default {
this.updateActivityBarView(view);
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
},
leftSidebarViews,
@@ -81,6 +82,9 @@ export default {
@click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)"
>
<gl-icon name="commit" />
+ <div v-if="stagedFiles.length > 0" class="ide-commit-badge badge badge-pill">
+ {{ stagedFiles.length }}
+ </div>
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
index c317fadb656..1ae7cf9339d 100644
--- a/app/assets/javascripts/ide/components/branches/search_list.vue
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -1,7 +1,7 @@
<script>
-import { mapActions, mapState } from 'vuex';
-import { debounce } from 'lodash';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { mapActions, mapState } from 'vuex';
import Item from './item.vue';
export default {
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index b89329c92ec..273d8d972f7 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,11 +1,14 @@
<script>
+import { GlSprintf } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
-import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import consts from '../../stores/modules/commit/constants';
-import RadioGroup from './radio_group.vue';
+import {
+ COMMIT_TO_CURRENT_BRANCH,
+ COMMIT_TO_NEW_BRANCH,
+} from '../../stores/modules/commit/constants';
import NewMergeRequestOption from './new_merge_request_option.vue';
+import RadioGroup from './radio_group.vue';
const { mapState: mapCommitState, mapActions: mapCommitActions } = createNamespacedHelpers(
'commit',
@@ -53,14 +56,14 @@ export default {
}
if (this.shouldDefaultToCurrentBranch) {
- this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
+ this.updateCommitAction(COMMIT_TO_CURRENT_BRANCH);
} else {
- this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
+ this.updateCommitAction(COMMIT_TO_NEW_BRANCH);
}
},
},
- commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
- commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
+ commitToCurrentBranch: COMMIT_TO_CURRENT_BRANCH,
+ commitToNewBranch: COMMIT_TO_NEW_BRANCH,
currentBranchPermissionsTooltip: s__(
"IDE|This option is disabled because you don't have write permissions for the current branch.",
),
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
index 53fac09ab66..75f02af28c4 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -1,9 +1,9 @@
<script>
-import { mapActions } from 'vuex';
import { GlModal, GlButton } from '@gitlab/ui';
+import { mapActions } from 'vuex';
import { sprintf, __ } from '~/locale';
-import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 7c3e522a488..cdb3eaff207 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -1,12 +1,16 @@
<script>
+import { GlModal, GlSafeHtmlDirective, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
-import { GlModal, GlSafeHtmlDirective, GlButton } from '@gitlab/ui';
-import { n__, __ } from '~/locale';
-import CommitMessageField from './message_field.vue';
-import Actions from './actions.vue';
-import SuccessMessage from './success_message.vue';
+import { n__, s__ } from '~/locale';
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
import { createUnexpectedCommitError } from '../../lib/errors';
+import Actions from './actions.vue';
+import CommitMessageField from './message_field.vue';
+import SuccessMessage from './success_message.vue';
+
+const MSG_CANNOT_PUSH_CODE = s__(
+ 'WebIDE|You need permission to edit files directly in this project.',
+);
export default {
components: {
@@ -18,6 +22,7 @@ export default {
},
directives: {
SafeHtml: GlSafeHtmlDirective,
+ GlTooltip: GlTooltipDirective,
},
data() {
return {
@@ -30,15 +35,21 @@ export default {
computed: {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading', 'commitError']),
- ...mapGetters(['someUncommittedChanges']),
+ ...mapGetters(['someUncommittedChanges', 'canPushCode']),
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
+ commitButtonDisabled() {
+ return !this.canPushCode || !this.someUncommittedChanges;
+ },
+ commitButtonTooltip() {
+ if (!this.canPushCode) {
+ return MSG_CANNOT_PUSH_CODE;
+ }
+
+ return '';
+ },
overviewText() {
return n__('%d changed file', '%d changed files', this.stagedFiles.length);
},
- commitButtonText() {
- return this.stagedFiles.length ? __('Commit') : __('Stage & Commit');
- },
-
currentViewIsCommitView() {
return this.currentActivityView === leftSidebarViews.commit.name;
},
@@ -73,6 +84,12 @@ export default {
'updateCommitAction',
]),
commit() {
+ // Even though the submit button will be disabled, we need to disable the submission
+ // since hitting enter on the branch name text input also submits the form.
+ if (!this.canPushCode) {
+ return false;
+ }
+
return this.commitChanges();
},
handleCompactState() {
@@ -113,6 +130,8 @@ export default {
this.componentHeight = null;
},
},
+ // Expose for tests
+ MSG_CANNOT_PUSH_CODE,
};
</script>
@@ -134,17 +153,22 @@ export default {
@after-enter="afterEndTransition"
>
<div v-if="isCompact" ref="compactEl" class="commit-form-compact">
- <gl-button
- :disabled="!someUncommittedChanges"
- category="primary"
- variant="info"
- block
- class="qa-begin-commit-button"
- data-testid="begin-commit-button"
- @click="beginCommit"
+ <div
+ v-gl-tooltip="{ title: commitButtonTooltip }"
+ data-testid="begin-commit-button-tooltip"
>
- {{ __('Commit…') }}
- </gl-button>
+ <gl-button
+ :disabled="commitButtonDisabled"
+ category="primary"
+ variant="info"
+ block
+ class="qa-begin-commit-button"
+ data-testid="begin-commit-button"
+ @click="beginCommit"
+ >
+ {{ __('Commit…') }}
+ </gl-button>
+ </div>
<p class="text-center bold">{{ overviewText }}</p>
</div>
<form v-else ref="formEl" @submit.prevent.stop="commit">
@@ -157,16 +181,29 @@ export default {
/>
<div class="clearfix gl-mt-5">
<actions />
+ <div
+ v-gl-tooltip="{ title: commitButtonTooltip }"
+ class="float-left"
+ data-testid="commit-button-tooltip"
+ >
+ <gl-button
+ :disabled="commitButtonDisabled"
+ :loading="submitCommitLoading"
+ data-testid="commit-button"
+ class="qa-commit-button"
+ category="primary"
+ variant="success"
+ @click="commit"
+ >
+ {{ __('Commit') }}
+ </gl-button>
+ </div>
<gl-button
- :loading="submitCommitLoading"
- class="float-left qa-commit-button"
- category="primary"
- variant="success"
- @click="commit"
+ v-if="!discardDraftButtonDisabled"
+ class="float-right"
+ data-testid="discard-draft"
+ @click="discardDraft"
>
- {{ __('Commit') }}
- </gl-button>
- <gl-button v-if="!discardDraftButtonDisabled" class="float-right" @click="discardDraft">
{{ __('Discard draft') }}
</gl-button>
<gl-button
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 729ff7c74ec..fbe353fc4ba 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions } from 'vuex';
import { GlModal, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import ListItem from './list_item.vue';
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 4192a002486..79b6fd1ec68 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,9 +1,9 @@
<script>
-import { mapActions } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { mapActions } from 'vuex';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import { viewerTypes } from '../../constants';
import getCommitIconMap from '../../commit_icon';
+import { viewerTypes } from '../../constants';
export default {
components: {
@@ -85,7 +85,11 @@ export default {
role="button"
@click="openFileInEditor"
>
- <span class="multi-file-commit-list-file-path d-flex align-items-center">
+ <span
+ class="multi-file-commit-list-file-path d-flex align-items-center"
+ data-qa-selector="file_to_commit_content"
+ :data-qa-file-name="file.name"
+ >
<file-icon :file-name="file.name" class="gl-mr-3" />
<template v-if="file.prevName && file.prevName !== file.name">
{{ file.prevName }} &#x2192;
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
index cdf49866982..121dae4b87e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue
@@ -1,6 +1,6 @@
<script>
-import { createNamespacedHelpers } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
+import { createNamespacedHelpers } from 'vuex';
import { s__ } from '~/locale';
const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNamespacedHelpers(
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 91cce44382c..039b4a54b26 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState, mapGetters } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
+import { mapActions, mapState, mapGetters } from 'vuex';
export default {
directives: {
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index 08635b43b91..d3a52f9f0cf 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
-import { mapActions } from 'vuex';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { mapActions } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
index fb0d00dc6a1..d80ad723fce 100644
--- a/app/assets/javascripts/ide/components/file_row_extra.vue
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -1,10 +1,10 @@
<script>
-import { mapGetters } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
import { n__ } from '~/locale';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
-import NewDropdown from './new_dropdown/index.vue';
import MrFileIcon from './mr_file_icon.vue';
+import NewDropdown from './new_dropdown/index.vue';
export default {
name: 'FileRowExtra',
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
index 772dab3fed3..ec61e3374d7 100644
--- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -1,7 +1,7 @@
<script>
+import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
-import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
export default {
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index aac899fde0d..2816f89d60d 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,7 +1,7 @@
<script>
+import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import {
WEBIDE_MARK_APP_START,
WEBIDE_MARK_FILE_FINISH,
@@ -10,13 +10,12 @@ import {
WEBIDE_MEASURE_BEFORE_VUE,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { modalTypes } from '../constants';
import eventHub from '../eventhub';
+import { measurePerformance } from '../utils';
import IdeSidebar from './ide_side_bar.vue';
import RepoEditor from './repo_editor.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
-import { measurePerformance } from '../utils';
eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () =>
measurePerformance(
@@ -26,10 +25,15 @@ eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () =>
),
);
+const MSG_CANNOT_PUSH_CODE = s__(
+ 'WebIDE|You need permission to edit files directly in this project. Fork this project to make your changes and submit a merge request.',
+);
+
export default {
components: {
IdeSidebar,
RepoEditor,
+ GlAlert,
GlButton,
GlLoadingIcon,
ErrorMessage: () => import(/* webpackChunkName: 'ide_runtime' */ './error_message.vue'),
@@ -59,12 +63,14 @@ export default {
'loading',
]),
...mapGetters([
+ 'canPushCode',
'activeFile',
'someUncommittedChanges',
'isCommitModeActive',
'allBlobs',
'emptyRepo',
'currentTree',
+ 'hasCurrentProject',
'editorTheme',
'getUrlForPath',
]),
@@ -110,6 +116,7 @@ export default {
this.loadDeferred = true;
},
},
+ MSG_CANNOT_PUSH_CODE,
};
</script>
@@ -118,6 +125,9 @@ export default {
class="ide position-relative d-flex flex-column align-items-stretch"
:class="{ [`theme-${themeName}`]: themeName }"
>
+ <gl-alert v-if="!canPushCode" :dismissible="false">{{
+ $options.MSG_CANNOT_PUSH_CODE
+ }}</gl-alert>
<error-message v-if="errorMessage" :message="errorMessage" />
<div class="ide-view flex-grow d-flex">
<template v-if="loadDeferred">
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
index 7d2f0acb08c..bea25d42756 100644
--- a/app/assets/javascripts/ide/components/ide_review.vue
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
-import IdeTreeList from './ide_tree_list.vue';
-import EditorModeDropdown from './editor_mode_dropdown.vue';
import { viewerTypes } from '../constants';
+import EditorModeDropdown from './editor_mode_dropdown.vue';
+import IdeTreeList from './ide_tree_list.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 135b28685ed..c3d6494692a 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,12 +1,12 @@
<script>
-import { mapState, mapGetters } from 'vuex';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
-import IdeTree from './ide_tree.vue';
-import ResizablePanel from './resizable_panel.vue';
+import { mapState, mapGetters } from 'vuex';
+import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants';
import ActivityBar from './activity_bar.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeProjectHeader from './ide_project_header.vue';
-import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants';
+import IdeTree from './ide_tree.vue';
+import ResizablePanel from './resizable_panel.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
index 9dbed0ace40..d8daf3b7ad6 100644
--- a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
+++ b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
@@ -1,7 +1,8 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { otherSide } from '../utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { SIDE_RIGHT } from '../constants';
+import { otherSide } from '../utils';
export default {
directives: {
@@ -50,7 +51,7 @@ export default {
},
clickTab(e, tab) {
e.currentTarget.blur();
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
if (this.isActiveTab(tab)) {
this.$emit('close');
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index ee292190e06..28ca1b6750f 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,13 +1,13 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { mapActions, mapState, mapGetters } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import IdeStatusList from './ide_status_list.vue';
-import IdeStatusMr from './ide_status_mr.vue';
+import { mapActions, mapState, mapGetters } from 'vuex';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import { rightSidebarViews } from '../constants';
+import IdeStatusList from './ide_status_list.vue';
+import IdeStatusMr from './ide_status_mr.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/ide_status_list.vue b/app/assets/javascripts/ide/components/ide_status_list.vue
index aa61c0d9b5e..da393b42dca 100644
--- a/app/assets/javascripts/ide/components/ide_status_list.vue
+++ b/app/assets/javascripts/ide/components/ide_status_list.vue
@@ -1,8 +1,8 @@
<script>
-import { mapGetters } from 'vuex';
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue';
+import { mapGetters } from 'vuex';
import { isTextFile, getFileEOL } from '~/ide/utils';
+import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index e563de6659a..2df998d7518 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -2,9 +2,9 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import { modalTypes, viewerTypes } from '../constants';
import IdeTreeList from './ide_tree_list.vue';
-import Upload from './new_dropdown/upload.vue';
import NewEntryButton from './new_dropdown/button.vue';
import NewModal from './new_dropdown/modal.vue';
+import Upload from './new_dropdown/upload.vue';
export default {
components: {
@@ -58,8 +58,9 @@ export default {
<new-entry-button
:label="__('New file')"
:show-label="false"
- class="d-flex border-0 p-0 mr-3 qa-new-file"
+ class="d-flex border-0 p-0 mr-3"
icon="doc-new"
+ data-qa-selector="new_file_button"
@click="createNewFile()"
/>
<upload
@@ -73,6 +74,7 @@ export default {
:show-label="false"
class="d-flex border-0 p-0"
icon="folder-new"
+ data-qa-selector="new_directory_button"
@click="createNewFolder()"
/>
</div>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index b67881b14f4..b987adc8bae 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -1,9 +1,9 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
-import FileTree from '~/vue_shared/components/file_tree.vue';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { WEBIDE_MARK_FILE_CLICKED } from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
+import FileTree from '~/vue_shared/components/file_tree.vue';
import IdeFileRow from './ide_file_row.vue';
import NavDropdown from './nav_dropdown.vue';
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 7f07a5dbe43..8e611503cb4 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -1,11 +1,11 @@
<script>
/* eslint-disable vue/no-v-html */
-import { mapActions, mapState } from 'vuex';
-import { throttle } from 'lodash';
import { GlTooltipDirective, GlButton, GlIcon } from '@gitlab/ui';
+import { throttle } from 'lodash';
+import { mapActions, mapState } from 'vuex';
import { __ } from '../../../locale';
-import ScrollButton from './detail/scroll_button.vue';
import JobDescription from './detail/description.vue';
+import ScrollButton from './detail/scroll_button.vue';
const scrollPositions = {
top: 0,
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
index 4e0912f3f44..0ce21c5c36c 100644
--- a/app/assets/javascripts/ide/components/jobs/list.vue
+++ b/app/assets/javascripts/ide/components/jobs/list.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import { mapActions } from 'vuex';
import Stage from './stage.vue';
export default {
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index 4b3c6e61e11..680e8841a1f 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -1,10 +1,10 @@
<script>
-import { mapActions, mapState } from 'vuex';
-import { debounce } from 'lodash';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
-import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
+import Item from './item.vue';
const SEARCH_TYPES = [
{ type: 'created', label: __('Created by me') },
diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue
index 6ff77e556c0..f5f0db3a7a3 100644
--- a/app/assets/javascripts/ide/components/nav_dropdown.vue
+++ b/app/assets/javascripts/ide/components/nav_dropdown.vue
@@ -1,8 +1,8 @@
<script>
import $ from 'jquery';
import { mapGetters } from 'vuex';
-import NavForm from './nav_form.vue';
import NavDropdownButton from './nav_dropdown_button.vue';
+import NavForm from './nav_form.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
index 116d3cec03e..0db43123562 100644
--- a/app/assets/javascripts/ide/components/nav_dropdown_button.vue
+++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState } from 'vuex';
import { GlIcon } from '@gitlab/ui';
+import { mapState } from 'vuex';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
const EMPTY_LABEL = '-';
diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue
index 70a92b8d3ab..62bb4841760 100644
--- a/app/assets/javascripts/ide/components/nav_form.vue
+++ b/app/assets/javascripts/ide/components/nav_form.vue
@@ -1,6 +1,6 @@
<script>
-import Tabs from '~/vue_shared/components/tabs/tabs';
import Tab from '~/vue_shared/components/tabs/tab.vue';
+import Tabs from '~/vue_shared/components/tabs/tabs';
import BranchesSearchList from './branches/search_list.vue';
import MergeRequestSearchList from './merge_requests/list.vue';
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 692878de5e1..bdd201aac1b 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -1,10 +1,10 @@
<script>
-import { mapActions } from 'vuex';
import { GlIcon } from '@gitlab/ui';
-import upload from './upload.vue';
-import ItemButton from './button.vue';
+import { mapActions } from 'vuex';
import { modalTypes } from '../../constants';
+import ItemButton from './button.vue';
import NewModal from './modal.vue';
+import upload from './upload.vue';
export default {
components: {
@@ -108,6 +108,7 @@ export default {
class="d-flex"
icon="remove"
icon-classes="mr-2"
+ data-qa-selector="delete_button"
@click="deleteEntry(path)"
/>
</li>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 22eefb6634f..cafb58b0e2c 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState, mapGetters } from 'vuex';
import { GlModal, GlButton } from '@gitlab/ui';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __, sprintf, s__ } from '~/locale';
import { modalTypes } from '../../constants';
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 5704129c10f..76d8a0aff3d 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -1,6 +1,6 @@
<script>
-import ItemButton from './button.vue';
import { isTextFile } from '~/ide/utils';
+import ItemButton from './button.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 46ef08a45a9..da2d4fbe7f0 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -1,13 +1,13 @@
<script>
import { mapGetters, mapState } from 'vuex';
import { __ } from '~/locale';
-import CollapsibleSidebar from './collapsible_sidebar.vue';
-import ResizablePanel from '../resizable_panel.vue';
import { rightSidebarViews, SIDEBAR_INIT_WIDTH, SIDEBAR_NAV_WIDTH } from '../../constants';
-import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue';
+import PipelinesList from '../pipelines/list.vue';
import Clientside from '../preview/clientside.vue';
+import ResizablePanel from '../resizable_panel.vue';
import TerminalView from '../terminal/view.vue';
+import CollapsibleSidebar from './collapsible_sidebar.vue';
// Need to add the width of the nav buttons since the resizable container contains those as well
const WIDTH = SIDEBAR_INIT_WIDTH + SIDEBAR_NAV_WIDTH;
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index a4a13389fbf..2526db0cd7b 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,6 +1,4 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
-import { escape } from 'lodash';
import {
GlLoadingIcon,
GlIcon,
@@ -10,13 +8,14 @@ import {
GlBadge,
GlAlert,
} from '@gitlab/ui';
+import { escape } from 'lodash';
+import { mapActions, mapGetters, mapState } from 'vuex';
+import IDEServices from '~/ide/services';
import { sprintf, __ } from '../../../locale';
-import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import EmptyState from '../../../pipelines/components/pipelines_list/empty_state.vue';
+import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import JobsList from '../jobs/list.vue';
-import IDEServices from '~/ide/services';
-
export default {
components: {
GlIcon,
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index 4c2a369226e..13f2e775fc3 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -1,13 +1,13 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { listen } from 'codesandbox-api';
import { isEmpty, debounce } from 'lodash';
import { Manager } from 'smooshpack';
-import { listen } from 'codesandbox-api';
-import { GlLoadingIcon } from '@gitlab/ui';
-import Navigator from './navigator.vue';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { packageJsonPath, LIVE_PREVIEW_DEBOUNCE } from '../../constants';
-import { createPathWithExt } from '../../utils';
import eventHub from '../../eventhub';
+import { createPathWithExt } from '../../utils';
+import Navigator from './navigator.vue';
export default {
components: {
@@ -165,7 +165,7 @@ export default {
</p>
<a
:href="links.webIDEHelpPagePath"
- class="btn btn-primary"
+ class="btn gl-button btn-confirm"
target="_blank"
rel="noopener noreferrer"
>
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
index 8986359427f..0c6cb041095 100644
--- a/app/assets/javascripts/ide/components/preview/navigator.vue
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -1,6 +1,6 @@
<script>
-import { listen } from 'codesandbox-api';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { listen } from 'codesandbox-api';
export default {
components: {
@@ -78,6 +78,7 @@ export default {
this.visitPath(this.path);
},
visitPath(path) {
+ // eslint-disable-next-line vue/no-mutating-props
this.manager.iframe.src = `${this.manager.bundlerURL}${path}`;
},
},
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 8092ef3bce6..854ff74d0af 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -1,8 +1,8 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import CommitFilesList from './commit_sidebar/list.vue';
-import EmptyState from './commit_sidebar/empty_state.vue';
import { stageKeys } from '../constants';
+import EmptyState from './commit_sidebar/empty_state.vue';
+import CommitFilesList from './commit_sidebar/list.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index a9c05f2e1ac..690060f5cb0 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,9 +1,7 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
-import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
-import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import { __ } from '~/locale';
import {
WEBIDE_MARK_FILE_CLICKED,
WEBIDE_MARK_REPO_EDITOR_START,
@@ -12,21 +10,23 @@ import {
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
-import eventHub from '../eventhub';
+import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
+import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import {
leftSidebarViews,
viewerTypes,
FILE_VIEW_MODE_EDITOR,
FILE_VIEW_MODE_PREVIEW,
} from '../constants';
+import eventHub from '../eventhub';
import Editor from '../lib/editor';
-import FileTemplatesBar from './file_templates/bar.vue';
-import { __ } from '~/locale';
-import { extractMarkdownImagesFromEntries } from '../stores/utils';
-import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
-import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
+import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
+import { extractMarkdownImagesFromEntries } from '../stores/utils';
+import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
+import FileTemplatesBar from './file_templates/bar.vue';
export default {
name: 'RepoEditor',
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index e3c41eee15e..d28751c9571 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -1,10 +1,10 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
import { GlIcon } from '@gitlab/ui';
+import { mapActions, mapGetters } from 'vuex';
import { __, sprintf } from '~/locale';
-import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
import FileStatusIcon from './repo_file_status_icon.vue';
export default {
diff --git a/app/assets/javascripts/ide/components/terminal/session.vue b/app/assets/javascripts/ide/components/terminal/session.vue
index 0e67a2ab45f..3a4128b6207 100644
--- a/app/assets/javascripts/ide/components/terminal/session.vue
+++ b/app/assets/javascripts/ide/components/terminal/session.vue
@@ -1,9 +1,9 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlButton } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
-import Terminal from './terminal.vue';
import { isEndingStatus } from '../../stores/modules/terminal/utils';
+import Terminal from './terminal.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/terminal/terminal.vue b/app/assets/javascripts/ide/components/terminal/terminal.vue
index 0ee4107f9ab..08fb2f5e5a0 100644
--- a/app/assets/javascripts/ide/components/terminal/terminal.vue
+++ b/app/assets/javascripts/ide/components/terminal/terminal.vue
@@ -1,11 +1,11 @@
<script>
-import { mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import { mapState } from 'vuex';
import { __ } from '~/locale';
import GLTerminal from '~/terminal/terminal';
-import TerminalControls from './terminal_controls.vue';
import { RUNNING, STOPPING } from '../../stores/modules/terminal/constants';
import { isStartingStatus } from '../../stores/modules/terminal/utils';
+import TerminalControls from './terminal_controls.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
index c3f722d6052..67692c842b8 100644
--- a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
+++ b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
@@ -1,6 +1,6 @@
<script>
-import { throttle } from 'lodash';
import { GlTooltipDirective, GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { throttle } from 'lodash';
import { mapState } from 'vuex';
import {
MSG_TERMINAL_SYNC_CONNECTING,
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index e5618466395..ed6b750480b 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -16,6 +16,13 @@ export const PERMISSION_CREATE_MR = 'createMergeRequestIn';
export const PERMISSION_READ_MR = 'readMergeRequest';
export const PERMISSION_PUSH_CODE = 'pushCode';
+// The default permission object to use when the project data isn't available yet.
+// This helps us encapsulate checks like `canPushCode` without requiring an
+// additional check like `currentProject && canPushCode`.
+export const DEFAULT_PERMISSIONS = {
+ [PERMISSION_PUSH_CODE]: true,
+};
+
export const viewerTypes = {
mr: 'mrdiff',
edit: 'editor',
@@ -100,3 +107,7 @@ export const SIDE_RIGHT = 'right';
// Live Preview feature
export const LIVE_PREVIEW_DEBOUNCE = 2000;
+
+// This is the maximum number of files to auto open when opening the Web IDE
+// from a Merge Request
+export const MAX_MR_FILES_AUTO_OPEN = 10;
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 0e6775d87f1..cb59cd7a8df 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -1,14 +1,14 @@
import Vue from 'vue';
+import { deprecatedCreateFlash as flash } from '~/flash';
import IdeRouter from '~/ide/ide_router_extension';
import { joinPaths } from '~/lib/utils/url_utility';
-import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
-import { performanceMarkAndMeasure } from '~/performance/utils';
import {
WEBIDE_MARK_FETCH_PROJECT_DATA_START,
WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
WEBIDE_MEASURE_FETCH_PROJECT_DATA,
} from '~/performance/constants';
+import { performanceMarkAndMeasure } from '~/performance/utils';
import { syncRouterAndStore } from './sync_router_and_store';
Vue.use(IdeRouter);
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index af408c06556..1b4b59ef62f 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -1,14 +1,14 @@
+import { identity } from 'lodash';
import Vue from 'vue';
import { mapActions } from 'vuex';
-import { identity } from 'lodash';
-import Translate from '~/vue_shared/translate';
import PerformancePlugin from '~/performance/vue_performance_plugin';
-import ide from './components/ide.vue';
-import { createStore } from './stores';
-import { createRouter } from './ide_router';
+import Translate from '~/vue_shared/translate';
import { parseBoolean } from '../lib/utils/common_utils';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
+import ide from './components/ide.vue';
+import { createRouter } from './ide_router';
import { DEFAULT_THEME } from './lib/themes';
+import { createStore } from './stores';
Vue.use(Translate);
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 4969875439e..46128651547 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -1,9 +1,9 @@
import { editor as monacoEditor, Uri } from 'monaco-editor';
-import Disposable from './disposable';
+import { insertFinalNewline } from '~/lib/utils/text_utility';
import eventHub from '../../eventhub';
import { trimTrailingWhitespace } from '../../utils';
-import { insertFinalNewline } from '~/lib/utils/text_utility';
import { defaultModelOptions } from '../editor_options';
+import Disposable from './disposable';
export default class Model {
constructor(file, head = null) {
diff --git a/app/assets/javascripts/ide/lib/create_diff.js b/app/assets/javascripts/ide/lib/create_diff.js
index 51d4967fb23..17779b6314e 100644
--- a/app/assets/javascripts/ide/lib/create_diff.js
+++ b/app/assets/javascripts/ide/lib/create_diff.js
@@ -1,5 +1,5 @@
-import { commitActionForFile } from '~/ide/stores/utils';
import { commitActionTypes } from '~/ide/constants';
+import { commitActionForFile } from '~/ide/stores/utils';
import createFileDiff from './create_file_diff';
const getDeletedParents = (entries, file) => {
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index 3efe692be13..1d051062637 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -1,7 +1,7 @@
-import { Range } from 'monaco-editor';
import { throttle } from 'lodash';
-import DirtyDiffWorker from './diff_worker';
+import { Range } from 'monaco-editor';
import Disposable from '../common/disposable';
+import DirtyDiffWorker from './diff_worker';
export const getDiffChangeType = (change) => {
if (change.modified) {
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 4fad0c09ce7..80191f635a3 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,15 +1,15 @@
import { debounce } from 'lodash';
import { editor as monacoEditor, KeyCode, KeyMod, Range } from 'monaco-editor';
-import DecorationsController from './decorations/controller';
-import DirtyDiffController from './diff/controller';
+import { clearDomElement } from '~/editor/utils';
+import { registerLanguages } from '../utils';
import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
+import DecorationsController from './decorations/controller';
+import DirtyDiffController from './diff/controller';
import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options';
-import { themes } from './themes';
-import languages from './languages';
import keymap from './keymap.json';
-import { clearDomElement } from '~/editor/utils';
-import { registerLanguages } from '../utils';
+import languages from './languages';
+import { themes } from './themes';
function setupThemes() {
themes.forEach((theme) => {
diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js
index f975034a872..a8a048e588f 100644
--- a/app/assets/javascripts/ide/lib/errors.js
+++ b/app/assets/javascripts/ide/lib/errors.js
@@ -1,6 +1,6 @@
import { escape } from 'lodash';
import { __ } from '~/locale';
-import consts from '../stores/modules/commit/constants';
+import { COMMIT_TO_NEW_BRANCH } from '../stores/modules/commit/constants';
const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/;
const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/;
@@ -8,7 +8,7 @@ const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/;
const createNewBranchAndCommit = (store) =>
store
- .dispatch('commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH)
+ .dispatch('commit/updateCommitAction', COMMIT_TO_NEW_BRANCH)
.then(() => store.dispatch('commit/commitChanges'));
export const createUnexpectedCommitError = (message) => ({
diff --git a/app/assets/javascripts/ide/lib/languages/index.js b/app/assets/javascripts/ide/lib/languages/index.js
index 580ad820bf9..f758cb7dd86 100644
--- a/app/assets/javascripts/ide/lib/languages/index.js
+++ b/app/assets/javascripts/ide/lib/languages/index.js
@@ -1,5 +1,5 @@
-import vue from './vue';
import hcl from './hcl';
+import vue from './vue';
const languages = [vue, hcl];
diff --git a/app/assets/javascripts/ide/lib/mirror.js b/app/assets/javascripts/ide/lib/mirror.js
index 6f9cfec9465..78990953beb 100644
--- a/app/assets/javascripts/ide/lib/mirror.js
+++ b/app/assets/javascripts/ide/lib/mirror.js
@@ -1,6 +1,6 @@
-import createDiff from './create_diff';
import { getWebSocketUrl, mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import createDiff from './create_diff';
export const SERVICE_NAME = 'webide-file-sync';
export const PROTOCOL = 'webfilesync.gitlab.com';
diff --git a/app/assets/javascripts/ide/lib/themes/index.js b/app/assets/javascripts/ide/lib/themes/index.js
index bb5be50576c..6a3feb9cab6 100644
--- a/app/assets/javascripts/ide/lib/themes/index.js
+++ b/app/assets/javascripts/ide/lib/themes/index.js
@@ -1,9 +1,9 @@
-import white from './white';
import dark from './dark';
import monokai from './monokai';
-import solarizedLight from './solarized_light';
-import solarizedDark from './solarized_dark';
import none from './none';
+import solarizedDark from './solarized_dark';
+import solarizedLight from './solarized_light';
+import white from './white';
export const themes = [
{
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 2264d63c737..e98653aedc2 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,6 +1,6 @@
+import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
-import Api from '~/api';
import getUserPermissions from '../queries/getUserPermissions.query.graphql';
import { query } from './gql';
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index d62dfc35d15..bf94f9d31c8 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,19 +1,19 @@
-import Vue from 'vue';
import { escape } from 'lodash';
-import { __, sprintf } from '~/locale';
-import { visitUrl } from '~/lib/utils/url_utility';
+import Vue from 'vue';
import { deprecatedCreateFlash as flash } from '~/flash';
-import { performanceMarkAndMeasure } from '~/performance/utils';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { __, sprintf } from '~/locale';
import {
WEBIDE_MARK_FETCH_BRANCH_DATA_START,
WEBIDE_MARK_FETCH_BRANCH_DATA_FINISH,
WEBIDE_MEASURE_FETCH_BRANCH_DATA,
} from '~/performance/constants';
-import * as types from './mutation_types';
-import { decorateFiles } from '../lib/files';
+import { performanceMarkAndMeasure } from '~/performance/utils';
import { stageKeys, commitActionTypes } from '../constants';
-import service from '../services';
import eventHub from '../eventhub';
+import { decorateFiles } from '../lib/files';
+import service from '../services';
+import * as types from './mutation_types';
export const redirectToUrl = (self, url) => visitUrl(url);
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 42668dec63a..d1e40920ebc 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -1,16 +1,16 @@
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import { performanceMarkAndMeasure } from '~/performance/utils';
import {
WEBIDE_MARK_FETCH_FILE_DATA_START,
WEBIDE_MARK_FETCH_FILE_DATA_FINISH,
WEBIDE_MEASURE_FETCH_FILE_DATA,
} from '~/performance/constants';
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import { viewerTypes, stageKeys, commitActionTypes } from '../../constants';
import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
import { setPageTitleForFile } from '../utils';
-import { viewerTypes, stageKeys, commitActionTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch, getters }, file) => {
const { path } = file;
@@ -120,10 +120,6 @@ export const getFileData = (
});
};
-export const setFileMrChange = ({ commit }, { file, mrChange }) => {
- commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
-};
-
export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => {
const file = state.entries[path];
const stagedFile = state.stagedFiles.find((f) => f.path === path);
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index 8215cba7ccf..753f6b9cd47 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -1,8 +1,8 @@
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
+import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '../../constants';
import service from '../../services';
import * as types from '../mutation_types';
-import { leftSidebarViews, PERMISSION_READ_MR } from '../../constants';
export const getMergeRequestsForBranch = (
{ commit, state, getters },
@@ -147,70 +147,96 @@ export const getMergeRequestVersions = (
}
});
-export const openMergeRequest = (
- { dispatch, state, getters },
- { projectId, targetProjectId, mergeRequestId } = {},
-) =>
- dispatch('getMergeRequestData', {
- projectId,
- targetProjectId,
- mergeRequestId,
- })
- .then((mr) => {
- dispatch('setCurrentBranchId', mr.source_branch);
-
- return dispatch('getBranchData', {
- projectId,
- branchId: mr.source_branch,
- }).then(() => {
- const branch = getters.findBranch(projectId, mr.source_branch);
-
- return dispatch('getFiles', {
- projectId,
- branchId: mr.source_branch,
- ref: branch.commit.id,
+export const openMergeRequestChanges = async ({ dispatch, getters, state, commit }, changes) => {
+ const entryChanges = changes
+ .map((change) => ({ entry: state.entries[change.new_path], change }))
+ .filter((x) => x.entry);
+
+ const pathsToOpen = entryChanges
+ .slice(0, MAX_MR_FILES_AUTO_OPEN)
+ .map(({ change }) => change.new_path);
+
+ // If there are no changes with entries, do nothing.
+ if (!entryChanges.length) {
+ return;
+ }
+
+ dispatch('updateActivityBarView', leftSidebarViews.review.name);
+
+ entryChanges.forEach(({ change, entry }) => {
+ commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file: entry, mrChange: change });
+ });
+
+ // Open paths in order as they appear in MR changes
+ pathsToOpen.forEach((path) => {
+ commit(types.TOGGLE_FILE_OPEN, path);
+ });
+
+ // Activate first path.
+ // We don't `getFileData` here since the editor component kicks that off. Otherwise, we'd fetch twice.
+ const [firstPath, ...remainingPaths] = pathsToOpen;
+ await dispatch('router/push', getters.getUrlForPath(firstPath));
+ await dispatch('setFileActive', firstPath);
+
+ // Lastly, eagerly fetch the remaining paths for improved user experience.
+ await Promise.all(
+ remainingPaths.map(async (path) => {
+ try {
+ await dispatch('getFileData', {
+ path,
+ makeFileActive: false,
});
- });
- })
- .then(() =>
- dispatch('getMergeRequestVersions', {
- projectId,
- targetProjectId,
- mergeRequestId,
- }),
- )
- .then(() =>
- dispatch('getMergeRequestChanges', {
- projectId,
- targetProjectId,
- mergeRequestId,
- }),
- )
- .then((mrChanges) => {
- if (mrChanges.changes.length) {
- dispatch('updateActivityBarView', leftSidebarViews.review.name);
+ await dispatch('getRawFileData', { path });
+ } catch (e) {
+ // If one of the file fetches fails, we dont want to blow up the rest of them.
+ // eslint-disable-next-line no-console
+ console.error('[gitlab] An unexpected error occurred fetching MR file data', e);
}
+ }),
+ );
+};
- mrChanges.changes.forEach((change, ind) => {
- const changeTreeEntry = state.entries[change.new_path];
+export const openMergeRequest = async (
+ { dispatch, getters },
+ { projectId, targetProjectId, mergeRequestId } = {},
+) => {
+ try {
+ const mr = await dispatch('getMergeRequestData', {
+ projectId,
+ targetProjectId,
+ mergeRequestId,
+ });
- if (changeTreeEntry) {
- dispatch('setFileMrChange', {
- file: changeTreeEntry,
- mrChange: change,
- });
+ dispatch('setCurrentBranchId', mr.source_branch);
- if (ind < 10) {
- dispatch('getFileData', {
- path: change.new_path,
- makeFileActive: ind === 0,
- openFile: true,
- });
- }
- }
- });
- })
- .catch((e) => {
- flash(__('Error while loading the merge request. Please try again.'));
- throw e;
+ await dispatch('getBranchData', {
+ projectId,
+ branchId: mr.source_branch,
+ });
+
+ const branch = getters.findBranch(projectId, mr.source_branch);
+
+ await dispatch('getFiles', {
+ projectId,
+ branchId: mr.source_branch,
+ ref: branch.commit.id,
});
+
+ await dispatch('getMergeRequestVersions', {
+ projectId,
+ targetProjectId,
+ mergeRequestId,
+ });
+
+ const { changes } = await dispatch('getMergeRequestChanges', {
+ projectId,
+ targetProjectId,
+ mergeRequestId,
+ });
+
+ await dispatch('openMergeRequestChanges', changes);
+ } catch (e) {
+ flash(__('Error while loading the merge request. Please try again.'));
+ throw e;
+ }
+};
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 27f6848f1d6..120a577d44a 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,8 +1,8 @@
import { escape } from 'lodash';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __, sprintf } from '~/locale';
-import service from '../../services';
import api from '../../../api';
+import service from '../../services';
import * as types from '../mutation_types';
export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) =>
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 150dfcb2726..f46d3cbe946 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -1,14 +1,14 @@
import { defer } from 'lodash';
-import { performanceMarkAndMeasure } from '~/performance/utils';
import {
WEBIDE_MARK_FETCH_FILES_FINISH,
WEBIDE_MEASURE_FETCH_FILES,
WEBIDE_MARK_FETCH_FILES_START,
} from '~/performance/constants';
+import { performanceMarkAndMeasure } from '~/performance/utils';
import { __ } from '../../../locale';
+import { decorateFiles } from '../../lib/files';
import service from '../../services';
import * as types from '../mutation_types';
-import { decorateFiles } from '../../lib/files';
export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_TREE_OPEN, path);
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 59e8d37a92a..9b93fc74f76 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,13 +1,14 @@
-import { getChangesCountForFiles, filePathMatches } from './utils';
+import Api from '~/api';
+import { addNumericSuffix } from '~/ide/utils';
import {
leftSidebarViews,
packageJsonPath,
+ DEFAULT_PERMISSIONS,
PERMISSION_READ_MR,
PERMISSION_CREATE_MR,
PERMISSION_PUSH_CODE,
} from '../constants';
-import { addNumericSuffix } from '~/ide/utils';
-import Api from '~/api';
+import { getChangesCountForFiles, filePathMatches } from './utils';
export const activeFile = (state) => state.openFiles.find((file) => file.active) || null;
@@ -150,7 +151,7 @@ export const getDiffInfo = (state, getters) => (path) => {
};
export const findProjectPermissions = (state, getters) => (projectId) =>
- getters.findProject(projectId)?.userPermissions || {};
+ getters.findProject(projectId)?.userPermissions || DEFAULT_PERMISSIONS;
export const canReadMergeRequests = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_READ_MR]);
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index d543209716a..b660ff178a2 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -1,19 +1,19 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import state from './state';
import * as actions from './actions';
import * as getters from './getters';
-import mutations from './mutations';
-import commitModule from './modules/commit';
-import pipelines from './modules/pipelines';
-import mergeRequests from './modules/merge_requests';
import branches from './modules/branches';
-import fileTemplates from './modules/file_templates';
-import paneModule from './modules/pane';
import clientsideModule from './modules/clientside';
-import routerModule from './modules/router';
+import commitModule from './modules/commit';
import editorModule from './modules/editor';
import { setupFileEditorsSync } from './modules/editor/setup';
+import fileTemplates from './modules/file_templates';
+import mergeRequests from './modules/merge_requests';
+import paneModule from './modules/pane';
+import pipelines from './modules/pipelines';
+import routerModule from './modules/router';
+import mutations from './mutations';
+import state from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/ide/stores/modules/branches/actions.js b/app/assets/javascripts/ide/stores/modules/branches/actions.js
index 74a4cd9848b..57a63749d7c 100644
--- a/app/assets/javascripts/ide/stores/modules/branches/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/branches/actions.js
@@ -1,5 +1,5 @@
-import { __ } from '~/locale';
import Api from '~/api';
+import { __ } from '~/locale';
import * as types from './mutation_types';
export const requestBranches = ({ commit }) => commit(types.REQUEST_BRANCHES);
diff --git a/app/assets/javascripts/ide/stores/modules/branches/index.js b/app/assets/javascripts/ide/stores/modules/branches/index.js
index deda95cd0c9..4b7d6d2998b 100644
--- a/app/assets/javascripts/ide/stores/modules/branches/index.js
+++ b/app/assets/javascripts/ide/stores/modules/branches/index.js
@@ -1,6 +1,6 @@
-import state from './state';
import * as actions from './actions';
import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 29b9a8a9521..29555799074 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,14 +1,14 @@
-import { sprintf, __ } from '~/locale';
import { deprecatedCreateFlash as flash } from '~/flash';
-import * as rootTypes from '../../mutation_types';
-import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
-import service from '../../../services';
-import * as types from './mutation_types';
-import consts from './constants';
+import { addNumericSuffix } from '~/ide/utils';
+import { sprintf, __ } from '~/locale';
import { leftSidebarViews } from '../../../constants';
import eventHub from '../../../eventhub';
import { parseCommitError } from '../../../lib/errors';
-import { addNumericSuffix } from '~/ide/utils';
+import service from '../../../services';
+import * as rootTypes from '../../mutation_types';
+import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
+import { COMMIT_TO_CURRENT_BRANCH } from './constants';
+import * as types from './mutation_types';
export const updateCommitMessage = ({ commit }, message) => {
commit(types.UPDATE_COMMIT_MESSAGE, message);
@@ -112,7 +112,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
// Pull commit options out because they could change
// During some of the pre and post commit processing
const { shouldCreateMR, shouldHideNewMrOption, isCreatingNewBranch, branchName } = getters;
- const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
+ const newBranch = state.commitAction !== COMMIT_TO_CURRENT_BRANCH;
const stageFilesPromise = rootState.stagedFiles.length
? Promise.resolve()
: dispatch('stageAllChanges', null, { root: true });
@@ -206,7 +206,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch('updateViewer', 'editor', { root: true });
}
})
- .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
+ .then(() => dispatch('updateCommitAction', COMMIT_TO_CURRENT_BRANCH))
.then(() => {
if (newBranch) {
const path = rootGetters.activeFile ? rootGetters.activeFile.path : '';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/constants.js b/app/assets/javascripts/ide/stores/modules/commit/constants.js
index c6c3701effe..9f4299e5537 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/constants.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/constants.js
@@ -1,7 +1,2 @@
-const COMMIT_TO_CURRENT_BRANCH = '1';
-const COMMIT_TO_NEW_BRANCH = '2';
-
-export default {
- COMMIT_TO_CURRENT_BRANCH,
- COMMIT_TO_NEW_BRANCH,
-};
+export const COMMIT_TO_CURRENT_BRANCH = '1';
+export const COMMIT_TO_NEW_BRANCH = '2';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index 2301cf23f9f..f5e367e16f5 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,5 +1,5 @@
import { sprintf, n__, __ } from '../../../../locale';
-import consts from './constants';
+import { COMMIT_TO_NEW_BRANCH } from './constants';
const BRANCH_SUFFIX_COUNT = 5;
const createTranslatedTextForFiles = (files, text) => {
@@ -48,7 +48,7 @@ export const preBuiltCommitMessage = (state, _, rootState) => {
.join('\n');
};
-export const isCreatingNewBranch = (state) => state.commitAction === consts.COMMIT_TO_NEW_BRANCH;
+export const isCreatingNewBranch = (state) => state.commitAction === COMMIT_TO_NEW_BRANCH;
export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) =>
!getters.isCreatingNewBranch &&
diff --git a/app/assets/javascripts/ide/stores/modules/commit/index.js b/app/assets/javascripts/ide/stores/modules/commit/index.js
index 5cec73bde2e..c5a39249348 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/index.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/index.js
@@ -1,7 +1,7 @@
-import state from './state';
-import mutations from './mutations';
import * as actions from './actions';
import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/ide/stores/modules/editor/index.js b/app/assets/javascripts/ide/stores/modules/editor/index.js
index 8a7437b427d..b601f0e79d4 100644
--- a/app/assets/javascripts/ide/stores/modules/editor/index.js
+++ b/app/assets/javascripts/ide/stores/modules/editor/index.js
@@ -1,7 +1,7 @@
import * as actions from './actions';
import * as getters from './getters';
-import state from './state';
import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/ide/stores/modules/editor/setup.js b/app/assets/javascripts/ide/stores/modules/editor/setup.js
index 9f3163aa6f5..5899706e38a 100644
--- a/app/assets/javascripts/ide/stores/modules/editor/setup.js
+++ b/app/assets/javascripts/ide/stores/modules/editor/setup.js
@@ -1,5 +1,5 @@
-import eventHub from '~/ide/eventhub';
import { commitActionTypes } from '~/ide/constants';
+import eventHub from '~/ide/eventhub';
const removeUnusedFileEditors = (store) => {
Object.keys(store.state.editor.fileEditors)
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
index 6800f824da0..bc96ec267b5 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
@@ -1,8 +1,8 @@
import Api from '~/api';
-import { __ } from '~/locale';
import { normalizeHeaders } from '~/lib/utils/common_utils';
-import * as types from './mutation_types';
+import { __ } from '~/locale';
import eventHub from '../../../eventhub';
+import * as types from './mutation_types';
export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
export const receiveTemplateTypesError = ({ commit, dispatch }) => {
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index 0613fe9b12b..9708e5e588c 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -1,5 +1,5 @@
-import { leftSidebarViews } from '../../../constants';
import { __ } from '~/locale';
+import { leftSidebarViews } from '../../../constants';
export const templateTypes = () => [
{
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/index.js b/app/assets/javascripts/ide/stores/modules/file_templates/index.js
index 383ff5db392..5f850b8b86a 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/index.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/index.js
@@ -1,7 +1,7 @@
-import createState from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
+import createState from './state';
export default () => ({
namespaced: true,
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
index 299f7a883d2..8446b93d14a 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
@@ -1,5 +1,5 @@
-import { __ } from '../../../../locale';
import Api from '../../../../api';
+import { __ } from '../../../../locale';
import { scopes } from './constants';
import * as types from './mutation_types';
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
index 04e7e0f08f1..d858a855d9e 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
@@ -1,6 +1,6 @@
-import state from './state';
import * as actions from './actions';
import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 2c2034d76d0..60561292c9d 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -1,10 +1,10 @@
-import Visibility from 'visibilityjs';
import axios from 'axios';
+import Visibility from 'visibilityjs';
import httpStatus from '../../../../lib/utils/http_status';
-import { __ } from '../../../../locale';
import Poll from '../../../../lib/utils/poll';
-import service from '../../../services';
+import { __ } from '../../../../locale';
import { rightSidebarViews } from '../../../constants';
+import service from '../../../services';
import * as types from './mutation_types';
let eTagPoll;
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/index.js b/app/assets/javascripts/ide/stores/modules/pipelines/index.js
index b44c3141b81..b48f63887ca 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/index.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/index.js
@@ -1,7 +1,7 @@
-import state from './state';
import * as actions from './actions';
-import mutations from './mutations';
import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/ide/stores/modules/router/index.js b/app/assets/javascripts/ide/stores/modules/router/index.js
index 68c81bb4509..1d5af1d4fe5 100644
--- a/app/assets/javascripts/ide/stores/modules/router/index.js
+++ b/app/assets/javascripts/ide/stores/modules/router/index.js
@@ -1,6 +1,6 @@
-import state from './state';
-import mutations from './mutations';
import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js
index b2c1ddd877c..91645a34a3d 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js
@@ -1,9 +1,9 @@
import Api from '~/api';
import httpStatus from '~/lib/utils/http_status';
-import * as types from '../mutation_types';
-import * as messages from '../messages';
-import { CHECK_CONFIG, CHECK_RUNNERS, RETRY_RUNNERS_INTERVAL } from '../constants';
import * as terminalService from '../../../../services/terminals';
+import { CHECK_CONFIG, CHECK_RUNNERS, RETRY_RUNNERS_INTERVAL } from '../constants';
+import * as messages from '../messages';
+import * as types from '../mutation_types';
export const requestConfigCheck = ({ commit }) => {
commit(types.REQUEST_CHECK, CHECK_CONFIG);
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
index aa460859b4c..6c9be6d10c9 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
@@ -1,10 +1,10 @@
+import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
-import { deprecatedCreateFlash as flash } from '~/flash';
-import * as types from '../mutation_types';
-import * as messages from '../messages';
import * as terminalService from '../../../../services/terminals';
import { STARTING, STOPPING, STOPPED } from '../constants';
+import * as messages from '../messages';
+import * as types from '../mutation_types';
export const requestStartSession = ({ commit }) => {
commit(types.SET_SESSION_STATUS, STARTING);
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js
index 3ab1817e662..da10894c2c6 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js
@@ -1,7 +1,7 @@
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
-import * as types from '../mutation_types';
+import axios from '~/lib/utils/axios_utils';
import * as messages from '../messages';
+import * as types from '../mutation_types';
import { isEndingStatus } from '../utils';
export const pollSessionStatus = ({ state, dispatch, commit }) => {
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/messages.js b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
index 967ba80cd2c..ec05ca84754 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/messages.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
@@ -1,6 +1,6 @@
import { escape } from 'lodash';
-import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
+import { __, sprintf } from '~/locale';
export const UNEXPECTED_ERROR_CONFIG = __(
'An unexpected error occurred while checking the project environment.',
diff --git a/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js b/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js
index 006800f58c2..a2cb0666a99 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import mirror, { canConnect } from '../../../lib/mirror';
+import * as types from './mutation_types';
export const upload = ({ rootState, commit }) => {
commit(types.START_LOADING);
diff --git a/app/assets/javascripts/ide/stores/modules/terminal_sync/index.js b/app/assets/javascripts/ide/stores/modules/terminal_sync/index.js
index 795c2fad724..a0685293839 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal_sync/index.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal_sync/index.js
@@ -1,6 +1,6 @@
-import state from './state';
import * as actions from './actions';
import mutations from './mutations';
+import state from './state';
export default () => ({
namespaced: true,
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 6ed6798a5b6..576f861a090 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
import * as types from './mutation_types';
-import projectMutations from './mutations/project';
-import mergeRequestMutation from './mutations/merge_request';
+import branchMutations from './mutations/branch';
import fileMutations from './mutations/file';
+import mergeRequestMutation from './mutations/merge_request';
+import projectMutations from './mutations/project';
import treeMutations from './mutations/tree';
-import branchMutations from './mutations/branch';
import {
sortTree,
swapInParentTreeWithSorting,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 4446971d5d6..fa9830a7469 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,7 +1,7 @@
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { diffModes } from '../../constants';
import * as types from '../mutation_types';
import { sortTree } from '../utils';
-import { diffModes } from '../../constants';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
diff --git a/app/assets/javascripts/ide/stores/plugins/terminal_sync.js b/app/assets/javascripts/ide/stores/plugins/terminal_sync.js
index d925a5f7567..944a034fe97 100644
--- a/app/assets/javascripts/ide/stores/plugins/terminal_sync.js
+++ b/app/assets/javascripts/ide/stores/plugins/terminal_sync.js
@@ -1,8 +1,8 @@
import { debounce } from 'lodash';
-import eventHub from '~/ide/eventhub';
import { commitActionTypes } from '~/ide/constants';
-import terminalSyncModule from '../modules/terminal_sync';
+import eventHub from '~/ide/eventhub';
import { isEndingStatus, isRunningStatus } from '../modules/terminal/utils';
+import terminalSyncModule from '../modules/terminal_sync';
const UPLOAD_DEBOUNCE = 200;
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 04eacf271b8..4019703b296 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -1,10 +1,10 @@
-import { commitActionTypes } from '../constants';
import {
relativePathToAbsolute,
isAbsolute,
isRootRelative,
isBlobUrl,
} from '~/lib/utils/url_utility';
+import { commitActionTypes } from '../constants';
export const dataStructure = () => ({
id: '',
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index 8eb2d17b876..63c53737119 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -1,7 +1,7 @@
-import { languages } from 'monaco-editor';
import { flatten, isString } from 'lodash';
-import { SIDE_LEFT, SIDE_RIGHT } from './constants';
+import { languages } from 'monaco-editor';
import { performanceMarkAndMeasure } from '~/performance/utils';
+import { SIDE_LEFT, SIDE_RIGHT } from './constants';
const toLowerCase = (x) => x.toLowerCase();
diff --git a/app/assets/javascripts/image_diff/helpers/init_image_diff.js b/app/assets/javascripts/image_diff/helpers/init_image_diff.js
index 8eef930c372..51168b94e6d 100644
--- a/app/assets/javascripts/image_diff/helpers/init_image_diff.js
+++ b/app/assets/javascripts/image_diff/helpers/init_image_diff.js
@@ -1,6 +1,6 @@
+import ImageFile from '../../commit/image_file';
import ImageDiff from '../image_diff';
import ReplacedImageDiff from '../replaced_image_diff';
-import ImageFile from '../../commit/image_file';
function initImageDiff(fileEl, canCreateNote, renderCommentBadge) {
const options = {
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
index 079f4a63f6e..a0dd8e6f894 100644
--- a/app/assets/javascripts/image_diff/image_diff.js
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import { isImageLoaded } from '../lib/utils/image_utility';
import imageDiffHelper from './helpers/index';
import ImageBadge from './image_badge';
-import { isImageLoaded } from '../lib/utils/image_utility';
export default class ImageDiff {
constructor(el, options) {
diff --git a/app/assets/javascripts/image_diff/replaced_image_diff.js b/app/assets/javascripts/image_diff/replaced_image_diff.js
index 2df15e5e1a5..a3d9b8a138a 100644
--- a/app/assets/javascripts/image_diff/replaced_image_diff.js
+++ b/app/assets/javascripts/image_diff/replaced_image_diff.js
@@ -1,6 +1,6 @@
import imageDiffHelper from './helpers/index';
-import { viewTypes, isValidViewType } from './view_types';
import ImageDiff from './image_diff';
+import { viewTypes, isValidViewType } from './view_types';
export default class ReplacedImageDiff extends ImageDiff {
init(defaultViewType = viewTypes.TWO_UP) {
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 80e2e73f420..7c5f48dcafc 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -1,78 +1,184 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
-import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
-import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql';
-import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql';
+import {
+ GlEmptyState,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlSearchBoxByClick,
+ GlSprintf,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql';
+import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql';
+import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql';
+import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
+import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
import ImportTableRow from './import_table_row.vue';
-const mapApolloMutations = (mutations) =>
- Object.fromEntries(
- Object.entries(mutations).map(([key, mutation]) => [
- key,
- function mutate(config) {
- return this.$apollo.mutate({
- mutation,
- ...config,
- });
- },
- ]),
- );
-
export default {
components: {
+ GlEmptyState,
+ GlIcon,
+ GlLink,
GlLoadingIcon,
+ GlSearchBoxByClick,
+ GlSprintf,
ImportTableRow,
+ PaginationLinks,
+ },
+
+ props: {
+ sourceUrl: {
+ type: String,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ filter: '',
+ page: 1,
+ };
},
apollo: {
- bulkImportSourceGroups: bulkImportSourceGroupsQuery,
+ bulkImportSourceGroups: {
+ query: bulkImportSourceGroupsQuery,
+ variables() {
+ return { page: this.page, filter: this.filter };
+ },
+ },
availableNamespaces: availableNamespacesQuery,
},
+ computed: {
+ hasGroups() {
+ return this.bulkImportSourceGroups?.nodes?.length > 0;
+ },
+
+ hasEmptyFilter() {
+ return this.filter.length > 0 && !this.hasGroups;
+ },
+
+ statusMessage() {
+ return this.filter.length === 0
+ ? s__('BulkImport|Showing %{start}-%{end} of %{total} from %{link}')
+ : s__(
+ 'BulkImport|Showing %{start}-%{end} of %{total} matching filter "%{filter}" from %{link}',
+ );
+ },
+
+ paginationInfo() {
+ const { page, perPage, total } = this.bulkImportSourceGroups?.pageInfo ?? {
+ page: 1,
+ perPage: 0,
+ total: 0,
+ };
+ const start = (page - 1) * perPage + 1;
+ const end = start + (this.bulkImportSourceGroups.nodes?.length ?? 0) - 1;
+
+ return { start, end, total };
+ },
+ },
+
+ watch: {
+ filter() {
+ this.page = 1;
+ },
+ },
+
methods: {
- ...mapApolloMutations({
- setTargetNamespace: setTargetNamespaceMutation,
- setNewName: setNewNameMutation,
- importGroup: importGroupMutation,
- }),
+ setPage(page) {
+ this.page = page;
+ },
+
+ updateTargetNamespace(sourceGroupId, targetNamespace) {
+ this.$apollo.mutate({
+ mutation: setTargetNamespaceMutation,
+ variables: { sourceGroupId, targetNamespace },
+ });
+ },
+
+ updateNewName(sourceGroupId, newName) {
+ this.$apollo.mutate({
+ mutation: setNewNameMutation,
+ variables: { sourceGroupId, newName },
+ });
+ },
+
+ importGroup(sourceGroupId) {
+ this.$apollo.mutate({
+ mutation: importGroupMutation,
+ variables: { sourceGroupId },
+ });
+ },
},
};
</script>
<template>
<div>
- <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
- <div v-else-if="bulkImportSourceGroups.length">
- <table class="gl-w-full">
- <thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1">
- <th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th>
- <th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th>
- <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th>
- <th class="gl-py-4 import-jobs-cta-col"></th>
- </thead>
- <tbody>
- <template v-for="group in bulkImportSourceGroups">
- <import-table-row
- :key="group.id"
- :group="group"
- :available-namespaces="availableNamespaces"
- @update-target-namespace="
- setTargetNamespace({
- variables: { sourceGroupId: group.id, targetNamespace: $event },
- })
- "
- @update-new-name="
- setNewName({
- variables: { sourceGroupId: group.id, newName: $event },
- })
- "
- @import-group="importGroup({ variables: { sourceGroupId: group.id } })"
- />
+ <div
+ class="gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex gl-align-items-center"
+ >
+ <span>
+ <gl-sprintf v-if="!$apollo.loading && hasGroups" :message="statusMessage">
+ <template #start>
+ <strong>{{ paginationInfo.start }}</strong>
+ </template>
+ <template #end>
+ <strong>{{ paginationInfo.end }}</strong>
</template>
- </tbody>
- </table>
+ <template #total>
+ <strong>{{ n__('%d group', '%d groups', paginationInfo.total) }}</strong>
+ </template>
+ <template #filter>
+ <strong>{{ filter }}</strong>
+ </template>
+ <template #link>
+ <gl-link class="gl-display-inline-block" :href="sourceUrl" target="_blank">
+ {{ sourceUrl }} <gl-icon name="external-link" class="vertical-align-middle" />
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ <gl-search-box-by-click class="gl-ml-auto" @submit="filter = $event" @clear="filter = ''" />
</div>
+ <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
+ <template v-else>
+ <gl-empty-state v-if="hasEmptyFilter" :title="__('Sorry, your filter produced no results')" />
+ <gl-empty-state
+ v-else-if="!hasGroups"
+ :title="s__('BulkImport|No groups available for import')"
+ />
+ <div v-else class="gl-display-flex gl-flex-direction-column gl-align-items-center">
+ <table class="gl-w-full">
+ <thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1">
+ <th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th>
+ <th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th>
+ <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th>
+ <th class="gl-py-4 import-jobs-cta-col"></th>
+ </thead>
+ <tbody>
+ <template v-for="group in bulkImportSourceGroups.nodes">
+ <import-table-row
+ :key="group.id"
+ :group="group"
+ :available-namespaces="availableNamespaces"
+ @update-target-namespace="updateTargetNamespace(group.id, $event)"
+ @update-new-name="updateNewName(group.id, $event)"
+ @import-group="importGroup(group.id)"
+ />
+ </template>
+ </tbody>
+ </table>
+ <pagination-links
+ :change="setPage"
+ :page-info="bulkImportSourceGroups.pageInfo"
+ class="gl-mt-3"
+ />
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
index 8f2d488d661..8110934efc4 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -1,15 +1,18 @@
-import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
import createDefaultClient from '~/lib/graphql';
+import axios from '~/lib/utils/axios_utils';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
-import createFlash from '~/flash';
import { STATUSES } from '../../constants';
import availableNamespacesQuery from './queries/available_namespaces.query.graphql';
import { SourceGroupsManager } from './services/source_groups_manager';
import { StatusPoller } from './services/status_poller';
export const clientTypenames = {
+ BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection',
BulkImportSourceGroup: 'ClientBulkImportSourceGroup',
AvailableNamespace: 'ClientAvailableNamespace',
+ BulkImportPageInfo: 'ClientBulkImportPageInfo',
};
export function createResolvers({ endpoints }) {
@@ -17,22 +20,47 @@ export function createResolvers({ endpoints }) {
return {
Query: {
- async bulkImportSourceGroups(_, __, { client }) {
+ async bulkImportSourceGroups(_, vars, { client }) {
const {
data: { availableNamespaces },
} = await client.query({ query: availableNamespacesQuery });
- return axios.get(endpoints.status).then(({ data }) => {
- return data.importable_data.map((group) => ({
- __typename: clientTypenames.BulkImportSourceGroup,
- ...group,
- status: STATUSES.NONE,
- import_target: {
- new_name: group.full_path,
- target_namespace: availableNamespaces[0].full_path,
+ if (!statusPoller) {
+ statusPoller = new StatusPoller({
+ client,
+ pollPath: endpoints.jobs,
+ });
+ statusPoller.startPolling();
+ }
+
+ return axios
+ .get(endpoints.status, {
+ params: {
+ page: vars.page,
+ per_page: vars.perPage,
+ filter: vars.filter,
},
- }));
- });
+ })
+ .then(({ headers, data }) => {
+ const pagination = parseIntPagination(normalizeHeaders(headers));
+
+ return {
+ __typename: clientTypenames.BulkImportSourceGroupConnection,
+ nodes: data.importable_data.map((group) => ({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ ...group,
+ status: STATUSES.NONE,
+ import_target: {
+ new_name: group.full_path,
+ target_namespace: availableNamespaces[0].full_path,
+ },
+ })),
+ pageInfo: {
+ __typename: clientTypenames.BulkImportPageInfo,
+ ...pagination,
+ },
+ };
+ });
},
availableNamespaces: () =>
@@ -63,7 +91,7 @@ export function createResolvers({ endpoints }) {
const group = groupManager.findById(sourceGroupId);
groupManager.setImportStatus(group, STATUSES.SCHEDULING);
try {
- await axios.post(endpoints.createBulkImport, {
+ const response = await axios.post(endpoints.createBulkImport, {
bulk_import: [
{
source_type: 'group_entity',
@@ -74,10 +102,7 @@ export function createResolvers({ endpoints }) {
],
});
groupManager.setImportStatus(group, STATUSES.STARTED);
- if (!statusPoller) {
- statusPoller = new StatusPoller({ client, interval: 3000 });
- statusPoller.startPolling();
- }
+ SourceGroupsManager.attachImportId(group, response.data.id);
} catch (e) {
createFlash({
message: s__('BulkImport|Importing the group failed'),
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql
index 8d52d94925c..28dfefdf8a7 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql
@@ -1,7 +1,15 @@
#import "../fragments/bulk_import_source_group_item.fragment.graphql"
-query bulkImportSourceGroups {
- bulkImportSourceGroups @client {
- ...BulkImportSourceGroupItem
+query bulkImportSourceGroups($page: Int = 1, $perPage: Int = 20, $filter: String = "") {
+ bulkImportSourceGroups(page: $page, filter: $filter, perPage: $perPage) @client {
+ nodes {
+ ...BulkImportSourceGroupItem
+ }
+ pageInfo {
+ page
+ perPage
+ total
+ totalPages
+ }
}
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
index 047b04fe7d6..261e30edbbb 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
@@ -14,6 +14,12 @@ function generateGroupId(id) {
}
export class SourceGroupsManager {
+ static importMap = new Map();
+
+ static attachImportId(group, importId) {
+ SourceGroupsManager.importMap.set(importId, group.id);
+ }
+
constructor({ client }) {
this.client = client;
}
@@ -36,6 +42,10 @@ export class SourceGroupsManager {
this.update(group, fn);
}
+ findByImportId(importId) {
+ return this.findById(SourceGroupsManager.importMap.get(importId));
+ }
+
setImportStatus(group, status) {
this.update(group, (sourceGroup) => {
// eslint-disable-next-line no-param-reassign
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
index 41dd25b9150..63cd6b48fc4 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
@@ -1,68 +1,47 @@
-import gql from 'graphql-tag';
+import Visibility from 'visibilityjs';
import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import Poll from '~/lib/utils/poll';
import { s__ } from '~/locale';
-import bulkImportSourceGroupsQuery from '../queries/bulk_import_source_groups.query.graphql';
-import { STATUSES } from '../../../constants';
import { SourceGroupsManager } from './source_groups_manager';
-const groupId = (i) => `group${i}`;
-
-function generateGroupsQuery(groups) {
- return gql`{
- ${groups
- .map(
- (g, idx) =>
- `${groupId(idx)}: group(fullPath: "${g.import_target.target_namespace}/${
- g.import_target.new_name
- }") { id }`,
- )
- .join('\n')}
- }`;
-}
-
export class StatusPoller {
- constructor({ client, interval }) {
+ constructor({ client, pollPath }) {
this.client = client;
- this.interval = interval;
- this.timeoutId = null;
- this.groupManager = new SourceGroupsManager({ client });
- }
- startPolling() {
- if (this.timeoutId) {
- return;
- }
+ this.eTagPoll = new Poll({
+ resource: {
+ fetchJobs: () => axios.get(pollPath),
+ },
+ method: 'fetchJobs',
+ successCallback: ({ data }) => this.updateImportsStatuses(data),
+ errorCallback: () =>
+ createFlash({
+ message: s__('BulkImport|Update of import statuses with realtime changes failed'),
+ }),
+ });
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.eTagPoll.restart();
+ } else {
+ this.eTagPoll.stop();
+ }
+ });
- this.checkPendingImports();
+ this.groupManager = new SourceGroupsManager({ client });
}
- stopPolling() {
- clearTimeout(this.timeoutId);
- this.timeoutId = null;
+ startPolling() {
+ this.eTagPoll.makeRequest();
}
- async checkPendingImports() {
- try {
- const { bulkImportSourceGroups } = this.client.readQuery({
- query: bulkImportSourceGroupsQuery,
- });
- const groupsInProgress = bulkImportSourceGroups.filter((g) => g.status === STATUSES.STARTED);
- if (groupsInProgress.length) {
- const { data: results } = await this.client.query({
- query: generateGroupsQuery(groupsInProgress),
- fetchPolicy: 'no-cache',
- });
- const completedGroups = groupsInProgress.filter((_, idx) => Boolean(results[groupId(idx)]));
- completedGroups.forEach((group) => {
- this.groupManager.setImportStatus(group, STATUSES.FINISHED);
- });
+ async updateImportsStatuses(importStatuses) {
+ importStatuses.forEach(({ id, status_name: statusName }) => {
+ const group = this.groupManager.findByImportId(id);
+ if (group.id) {
+ this.groupManager.setImportStatus(group, statusName);
}
- } catch (e) {
- createFlash({
- message: s__('BulkImport|Update of import statuses with realtime changes failed'),
- });
- } finally {
- this.timeoutId = setTimeout(() => this.checkPendingImports(), this.interval);
- }
+ });
}
}
diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
index bf427075564..cd837a840e4 100644
--- a/app/assets/javascripts/import_entities/import_groups/index.js
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Translate from '~/vue_shared/translate';
-import { createApolloClient } from './graphql/client_factory';
import ImportTable from './components/import_table.vue';
+import { createApolloClient } from './graphql/client_factory';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -10,13 +10,20 @@ Vue.use(VueApollo);
export function mountImportGroupsApp(mountElement) {
if (!mountElement) return undefined;
- const { statusPath, availableNamespacesPath, createBulkImportPath } = mountElement.dataset;
+ const {
+ statusPath,
+ availableNamespacesPath,
+ createBulkImportPath,
+ jobsPath,
+ sourceUrl,
+ } = mountElement.dataset;
const apolloProvider = new VueApollo({
defaultClient: createApolloClient({
endpoints: {
status: statusPath,
availableNamespaces: availableNamespacesPath,
createBulkImport: createBulkImportPath,
+ jobs: jobsPath,
},
}),
});
@@ -25,7 +32,11 @@ export function mountImportGroupsApp(mountElement) {
el: mountElement,
apolloProvider,
render(createElement) {
- return createElement(ImportTable);
+ return createElement(ImportTable, {
+ props: {
+ sourceUrl,
+ },
+ });
},
});
}
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 192d6e056cd..be09052fb7e 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -1,6 +1,6 @@
<script>
+import { GlButton, GlLoadingIcon, GlIntersectionObserver, GlModal, GlFormInput } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlButton, GlLoadingIcon, GlIntersectionObserver, GlModal } from '@gitlab/ui';
import { n__, __, sprintf } from '~/locale';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
@@ -12,6 +12,7 @@ export default {
GlButton,
GlModal,
GlIntersectionObserver,
+ GlFormInput,
},
props: {
providerTitle: {
@@ -115,13 +116,13 @@ export default {
<template>
<div>
- <p class="light text-nowrap mt-2">
+ <p class="gl-text-gray-900 gl-white-space-nowrap gl-mt-3">
{{ s__('ImportProjects|Select the repositories you want to import') }}
</p>
<template v-if="hasIncompatibleRepos">
<slot name="incompatible-repos-warning"></slot>
</template>
- <div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
+ <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap gl-mb-5">
<gl-button
variant="success"
:loading="isImportingAnyRepo"
@@ -148,24 +149,29 @@ export default {
<slot name="actions"></slot>
<form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
- <input
+ <gl-form-input
data-qa-selector="githubish_import_filter_field"
- class="form-control"
name="filter"
:placeholder="__('Filter your repositories by name')"
autofocus
- size="40"
+ size="lg"
@keyup.enter="setFilter($event.target.value)"
/>
</form>
</div>
- <div v-if="repositories.length" class="table-responsive">
- <table class="table import-table">
- <thead>
- <th class="import-jobs-from-col">{{ fromHeaderText }}</th>
- <th class="import-jobs-to-col">{{ __('To GitLab') }}</th>
- <th class="import-jobs-status-col">{{ __('Status') }}</th>
- <th class="import-jobs-cta-col"></th>
+ <div v-if="repositories.length" class="gl-w-full">
+ <table>
+ <thead class="gl-border-0 gl-border-solid gl-border-t-1 gl-border-gray-100">
+ <th class="import-jobs-from-col gl-p-4 gl-vertical-align-top gl-border-b-1">
+ {{ fromHeaderText }}
+ </th>
+ <th class="import-jobs-to-col gl-p-4 gl-vertical-align-top gl-border-b-1">
+ {{ __('To GitLab') }}
+ </th>
+ <th class="import-jobs-status-col gl-p-4 gl-vertical-align-top gl-border-b-1">
+ {{ __('Status') }}
+ </th>
+ <th class="import-jobs-cta-col gl-p-4 gl-vertical-align-top gl-border-b-1"></th>
</thead>
<tbody>
<template v-for="repo in repositories">
@@ -183,9 +189,9 @@ export default {
:key="pagePaginationStateKey"
@appear="fetchRepos"
/>
- <gl-loading-icon v-if="isLoading" class="import-projects-loading-icon" size="md" />
+ <gl-loading-icon v-if="isLoading" class="gl-mt-7" size="md" />
- <div v-if="!isLoading && repositories.length === 0" class="text-center">
+ <div v-if="!isLoading && repositories.length === 0" class="gl-text-center">
<strong>{{ emptyStateText }}</strong>
</div>
</div>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index 983abda57f7..289c83979bb 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -1,8 +1,8 @@
<script>
+import { GlIcon, GlBadge, GlFormInput, GlButton, GlLink } from '@gitlab/ui';
import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlIcon, GlBadge } from '@gitlab/ui';
-import Select2Select from '~/vue_shared/components/select2_select.vue';
import { __ } from '~/locale';
+import Select2Select from '~/vue_shared/components/select2_select.vue';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
@@ -12,8 +12,11 @@ export default {
components: {
Select2Select,
ImportStatus,
+ GlFormInput,
+ GlButton,
GlIcon,
GlBadge,
+ GlLink,
},
props: {
repo: {
@@ -61,7 +64,7 @@ export default {
select2Options() {
return {
data: this.availableNamespaces,
- containerCssClass: 'import-namespace-select qa-project-namespace-select w-auto',
+ containerCssClass: 'import-namespace-select qa-project-namespace-select gl-w-auto',
};
},
@@ -97,52 +100,56 @@ export default {
</script>
<template>
- <tr class="qa-project-import-row import-row">
- <td>
- <a
- :href="repo.importSource.providerLink"
- rel="noreferrer noopener"
- target="_blank"
- data-testid="providerLink"
+ <tr
+ class="qa-project-import-row gl-h-11 gl-border-0 gl-border-solid gl-border-t-1 gl-border-gray-100 gl-h-11"
+ >
+ <td class="gl-p-4">
+ <gl-link :href="repo.importSource.providerLink" target="_blank" data-testid="providerLink"
>{{ repo.importSource.fullName }}
<gl-icon v-if="repo.importSource.providerLink" name="external-link" />
- </a>
+ </gl-link>
</td>
- <td class="d-flex flex-wrap flex-lg-nowrap" data-testid="fullPath">
+ <td
+ class="gl-display-flex gl-flex-sm-wrap gl-p-4 gl-pt-5 gl-vertical-align-top"
+ data-testid="fullPath"
+ >
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted">
- <select2-select v-model="targetNamespaceSelect" :options="select2Options" />
- <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
- >/</span
- >
- <input
- v-model="newNameInput"
- type="text"
- class="form-control import-project-name-input qa-project-path-field"
- />
+ <div class="import-entities-target-select gl-display-flex gl-align-items-stretch gl-w-full">
+ <select2-select v-model="targetNamespaceSelect" :options="select2Options" />
+ <div
+ class="import-entities-target-select-separator gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
+ >
+ /
+ </div>
+ <gl-form-input
+ v-model="newNameInput"
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none qa-project-path-field"
+ />
+ </div>
</template>
<template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</td>
- <td>
+ <td class="gl-p-4">
<import-status :status="importStatus" />
</td>
<td data-testid="actions">
- <a
+ <gl-button
v-if="isFinished"
class="btn btn-default"
:href="repo.importedProject.fullPath"
rel="noreferrer noopener"
target="_blank"
>{{ __('Go to project') }}
- </a>
- <button
+ </gl-button>
+ <gl-button
v-if="isImportNotStarted"
type="button"
- class="qa-import-button btn btn-default"
+ class="qa-import-button"
@click="fetchImport(repo.importSource.id)"
>
{{ importButtonText }}
- </button>
+ </gl-button>
<gl-badge v-else-if="isIncompatible" variant="danger">{{
__('Incompatible project')
}}</gl-badge>
diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js
index 7373b628f2b..6b7fe23ed60 100644
--- a/app/assets/javascripts/import_entities/import_projects/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
import { parseBoolean } from '~/lib/utils/common_utils';
+import Translate from '~/vue_shared/translate';
import ImportProjectsTable from './components/import_projects_table.vue';
import createStore from './store';
diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js
index a8217ff1033..33f8dbb8737 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -1,14 +1,14 @@
import Visibility from 'visibilityjs';
-import * as types from './mutation_types';
-import { isProjectImportable } from '../utils';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import Poll from '~/lib/utils/poll';
-import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import Poll from '~/lib/utils/poll';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
+import { s__, sprintf } from '~/locale';
+import { isProjectImportable } from '../utils';
+import * as types from './mutation_types';
let eTagPoll;
diff --git a/app/assets/javascripts/import_entities/import_projects/store/index.js b/app/assets/javascripts/import_entities/import_projects/store/index.js
index 7ba12f81eb9..a2880e7d031 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import state from './state';
import actionsFactory from './actions';
import * as getters from './getters';
import mutations from './mutations';
+import state from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutations.js b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
index 1a96508bd48..c5e1922597a 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import * as types from './mutation_types';
import { STATUSES } from '../../constants';
+import * as types from './mutation_types';
const makeNewImportedProject = (importedProject) => ({
importSource: {
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 7d44a28b4bb..5638dc064d1 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -10,22 +10,20 @@ import {
GlIcon,
GlEmptyState,
} from '@gitlab/ui';
+import { convertToSnakeCase } from '~/lib/utils/text_utility';
+import { visitUrl, mergeUrlParams, joinPaths } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
+import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
+import SeverityToken from '~/sidebar/components/severity/severity.vue';
import Tracking from '~/tracking';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import {
tdClass,
thClass,
bodyTrClass,
initialPaginationState,
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
-import { convertToSnakeCase } from '~/lib/utils/text_utility';
-import { s__ } from '~/locale';
-import { visitUrl, mergeUrlParams, joinPaths } from '~/lib/utils/url_utility';
-import getIncidents from '../graphql/queries/get_incidents.query.graphql';
-import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
-import SeverityToken from '~/sidebar/components/severity/severity.vue';
-import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
+import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
I18N,
INCIDENT_STATUS_TABS,
@@ -37,6 +35,8 @@ import {
trackIncidentCreateNewOptions,
trackIncidentListViewsOptions,
} from '../constants';
+import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
+import getIncidents from '../graphql/queries/get_incidents.query.graphql';
export default {
trackIncidentCreateNewOptions,
@@ -102,7 +102,7 @@ export default {
GlIcon,
PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'),
ServiceLevelAgreementCell: () =>
- import('ee_component/incidents/components/service_level_agreement_cell.vue'),
+ import('ee_component/vue_shared/components/incidents/service_level_agreement.vue'),
GlEmptyState,
SeverityToken,
PaginatedTableWithSearchAndTabs,
diff --git a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
index c90ff8079b8..9d5f37dc3b7 100644
--- a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
+++ b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton, GlTabs, GlTab } from '@gitlab/ui';
+import { INTEGRATION_TABS_CONFIG, I18N_INTEGRATION_TABS } from '../constants';
import AlertsSettingsForm from './alerts_form.vue';
import PagerDutySettingsForm from './pagerduty_form.vue';
-import { INTEGRATION_TABS_CONFIG, I18N_INTEGRATION_TABS } from '../constants';
export default {
components: {
@@ -26,7 +26,7 @@ export default {
class="settings no-animate qa-incident-management-settings"
>
<div class="settings-header">
- <h4 ref="sectionHeader" class="gl-my-3! gl-py-1">
+ <h4 ref="sectionHeader">
{{ $options.i18n.headerText }}
</h4>
<gl-button ref="toggleBtn" class="js-settings-toggle">{{
diff --git a/app/assets/javascripts/incidents_settings/incidents_settings_service.js b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
index f0e692f9cbe..82b94c08381 100644
--- a/app/assets/javascripts/incidents_settings/incidents_settings_service.js
+++ b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
@@ -1,6 +1,6 @@
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import { ERROR_MSG } from './constants';
export default class IncidentsSettingsService {
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
index 22667d8ae88..b42264c870b 100644
--- a/app/assets/javascripts/init_changes_dropdown.js
+++ b/app/assets/javascripts/init_changes_dropdown.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { stickyMonitor } from './lib/utils/sticky';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { stickyMonitor } from './lib/utils/sticky';
export default (stickyTop) => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index 1e82ecb05b5..59038b3d9fb 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -1,11 +1,11 @@
/* eslint-disable no-new */
-import MilestoneSelect from './milestone_select';
-import LabelsSelect from './labels_select';
+import { mountSidebarLabels, getSidebarOptions } from '~/sidebar/mount_sidebar';
+import DueDateSelectors from './due_date_select';
import IssuableContext from './issuable_context';
+import LabelsSelect from './labels_select';
+import MilestoneSelect from './milestone_select';
import Sidebar from './right_sidebar';
-import DueDateSelectors from './due_date_select';
-import { mountSidebarLabels, getSidebarOptions } from '~/sidebar/mount_sidebar';
export default () => {
const sidebarOptEl = document.querySelector('.js-sidebar-options');
diff --git a/app/assets/javascripts/init_labels.js b/app/assets/javascripts/init_labels.js
index 15da5d5cceb..10bfbf7960c 100644
--- a/app/assets/javascripts/init_labels.js
+++ b/app/assets/javascripts/init_labels.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import LabelManager from './label_manager';
import GroupLabelSubscription from './group_label_subscription';
+import LabelManager from './label_manager';
import ProjectLabelSubscription from './project_label_subscription';
export default () => {
diff --git a/app/assets/javascripts/integrations/edit/components/active_checkbox.vue b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
index 6698984d02f..8677f139723 100644
--- a/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
+++ b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlFormGroup, GlFormCheckbox } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
import eventHub from '../event_hub';
export default {
diff --git a/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
index 93ea1f4f636..bcf4b036fd2 100644
--- a/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
+++ b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlModal } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
import { __ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index f568f7e6d3d..a4baca20ac9 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -1,10 +1,10 @@
<script>
/* eslint-disable vue/no-v-html */
-import { mapGetters } from 'vuex';
-import { capitalize, lowerCase, isEmpty } from 'lodash';
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
-import eventHub from '../event_hub';
+import { capitalize, lowerCase, isEmpty } from 'lodash';
+import { mapGetters } from 'vuex';
import { __, sprintf } from '~/locale';
+import eventHub from '../event_hub';
export default {
name: 'DynamicField',
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index ac8a64d5f3b..3ec0c23e55d 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,18 +1,18 @@
<script>
+import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
-import { GlButton, GlModalDirective } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import eventHub from '../event_hub';
import { integrationLevels } from '../constants';
+import eventHub from '../event_hub';
-import OverrideDropdown from './override_dropdown.vue';
import ActiveCheckbox from './active_checkbox.vue';
-import JiraTriggerFields from './jira_trigger_fields.vue';
-import JiraIssuesFields from './jira_issues_fields.vue';
-import TriggerFields from './trigger_fields.vue';
-import DynamicField from './dynamic_field.vue';
import ConfirmationModal from './confirmation_modal.vue';
+import DynamicField from './dynamic_field.vue';
+import JiraIssuesFields from './jira_issues_fields.vue';
+import JiraTriggerFields from './jira_trigger_fields.vue';
+import OverrideDropdown from './override_dropdown.vue';
import ResetConfirmationModal from './reset_confirmation_modal.vue';
+import TriggerFields from './trigger_fields.vue';
export default {
name: 'IntegrationForm',
@@ -28,9 +28,17 @@ export default {
GlButton,
},
directives: {
- 'gl-modal': GlModalDirective,
+ GlModal: GlModalDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
+ props: {
+ helpHtml: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
computed: {
...mapGetters(['currentKey', 'propsSource', 'isDisabled']),
...mapState([
@@ -80,11 +88,14 @@ export default {
this.fetchResetIntegration();
},
},
+ helpHtmlConfig: {
+ ADD_TAGS: ['use'], // to support icon SVGs
+ },
};
</script>
<template>
- <div>
+ <div class="gl-mb-3">
<override-dropdown
v-if="defaultState !== null"
:inherit-from-id="defaultState.id"
@@ -92,80 +103,92 @@ export default {
:learn-more-path="propsSource.learnMorePath"
@change="setOverride"
/>
- <active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" />
- <jira-trigger-fields
- v-if="isJira"
- :key="`${currentKey}-jira-trigger-fields`"
- v-bind="propsSource.triggerFieldsProps"
- />
- <trigger-fields
- v-else-if="propsSource.triggerEvents.length"
- :key="`${currentKey}-trigger-fields`"
- :events="propsSource.triggerEvents"
- :type="propsSource.type"
- />
- <dynamic-field
- v-for="field in propsSource.fields"
- :key="`${currentKey}-${field.name}`"
- v-bind="field"
- />
- <jira-issues-fields
- v-if="showJiraIssuesFields"
- :key="`${currentKey}-jira-issues-fields`"
- v-bind="propsSource.jiraIssuesProps"
- />
- <div v-if="isEditable" class="footer-block row-content-block">
- <template v-if="isInstanceOrGroupLevel">
- <gl-button
- v-gl-modal.confirmSaveIntegration
- category="primary"
- variant="success"
- :loading="isSaving"
- :disabled="isDisabled"
- data-qa-selector="save_changes_button"
- >
- {{ __('Save changes') }}
- </gl-button>
- <confirmation-modal @submit="onSaveClick" />
- </template>
- <gl-button
- v-else
- category="primary"
- variant="success"
- type="submit"
- :loading="isSaving"
- :disabled="isDisabled"
- data-qa-selector="save_changes_button"
- @click.prevent="onSaveClick"
- >
- {{ __('Save changes') }}
- </gl-button>
- <gl-button
- v-if="propsSource.canTest"
- :loading="isTesting"
- :disabled="isDisabled"
- :href="propsSource.testPath"
- @click.prevent="onTestClick"
- >
- {{ __('Test settings') }}
- </gl-button>
+ <div class="row">
+ <div class="col-lg-4"></div>
+
+ <div class="col-lg-8">
+ <!-- helpHtml is trusted input -->
+ <div v-if="helpHtml" v-safe-html:[$options.helpHtmlConfig]="helpHtml"></div>
+
+ <active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" />
+ <jira-trigger-fields
+ v-if="isJira"
+ :key="`${currentKey}-jira-trigger-fields`"
+ v-bind="propsSource.triggerFieldsProps"
+ />
+ <trigger-fields
+ v-else-if="propsSource.triggerEvents.length"
+ :key="`${currentKey}-trigger-fields`"
+ :events="propsSource.triggerEvents"
+ :type="propsSource.type"
+ />
+ <dynamic-field
+ v-for="field in propsSource.fields"
+ :key="`${currentKey}-${field.name}`"
+ v-bind="field"
+ />
+ <jira-issues-fields
+ v-if="showJiraIssuesFields"
+ :key="`${currentKey}-jira-issues-fields`"
+ v-bind="propsSource.jiraIssuesProps"
+ />
+ <div v-if="isEditable" class="footer-block row-content-block">
+ <template v-if="isInstanceOrGroupLevel">
+ <gl-button
+ v-gl-modal.confirmSaveIntegration
+ category="primary"
+ variant="success"
+ :loading="isSaving"
+ :disabled="isDisabled"
+ data-qa-selector="save_changes_button"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <confirmation-modal @submit="onSaveClick" />
+ </template>
+ <gl-button
+ v-else
+ category="primary"
+ variant="success"
+ type="submit"
+ :loading="isSaving"
+ :disabled="isDisabled"
+ data-qa-selector="save_changes_button"
+ @click.prevent="onSaveClick"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+
+ <gl-button
+ v-if="propsSource.canTest"
+ :loading="isTesting"
+ :disabled="isDisabled"
+ :href="propsSource.testPath"
+ @click.prevent="onTestClick"
+ >
+ {{ __('Test settings') }}
+ </gl-button>
- <template v-if="showReset">
- <gl-button
- v-gl-modal.confirmResetIntegration
- category="secondary"
- variant="default"
- :loading="isResetting"
- :disabled="isDisabled"
- data-testid="reset-button"
- >
- {{ __('Reset') }}
- </gl-button>
- <reset-confirmation-modal @reset="onResetClick" />
- </template>
+ <template v-if="showReset">
+ <gl-button
+ v-gl-modal.confirmResetIntegration
+ category="secondary"
+ variant="default"
+ :loading="isResetting"
+ :disabled="isDisabled"
+ data-testid="reset-button"
+ >
+ {{ __('Reset') }}
+ </gl-button>
+ <reset-confirmation-modal @reset="onResetClick" />
+ </template>
- <gl-button class="btn-cancel" :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
+ <gl-button class="btn-cancel" :href="propsSource.cancelPath">{{
+ __('Cancel')
+ }}</gl-button>
+ </div>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index 1baa2b440b0..d3d1fd8ddc3 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -8,6 +8,7 @@ import {
GlButton,
GlCard,
} from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
export default {
@@ -20,18 +21,36 @@ export default {
GlLink,
GlButton,
GlCard,
+ JiraIssueCreationVulnerabilities: () =>
+ import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
props: {
showJiraIssuesIntegration: {
type: Boolean,
required: false,
default: false,
},
+ showJiraVulnerabilitiesIntegration: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
initialEnableJiraIssues: {
type: Boolean,
required: false,
default: null,
},
+ initialEnableJiraVulnerabilities: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ initialVulnerabilitiesIssuetype: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
initialProjectKey: {
type: String,
required: false,
@@ -45,12 +64,12 @@ export default {
upgradePlanPath: {
type: String,
required: false,
- default: null,
+ default: '',
},
editProjectPath: {
type: String,
required: false,
- default: null,
+ default: '',
},
},
data() {
@@ -64,6 +83,13 @@ export default {
validProjectKey() {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
},
+ showJiraVulnerabilitiesOptions() {
+ return (
+ this.enableJiraIssues &&
+ this.showJiraVulnerabilitiesIntegration &&
+ this.glFeatures.jiraForVulnerabilities
+ );
+ },
},
created() {
eventHub.$on('validateForm', this.validateForm);
@@ -75,6 +101,9 @@ export default {
validateForm() {
this.validated = true;
},
+ getJiraIssueTypes() {
+ eventHub.$emit('getJiraIssueTypes');
+ },
},
};
</script>
@@ -105,6 +134,14 @@ export default {
}}
</template>
</gl-form-checkbox>
+ <jira-issue-creation-vulnerabilities
+ v-if="showJiraVulnerabilitiesOptions"
+ :project-key="projectKey"
+ :initial-is-enabled="initialEnableJiraVulnerabilities"
+ :initial-issue-type-id="initialVulnerabilitiesIssuetype"
+ data-testid="jira-for-vulnerabilities"
+ @request-get-issue-types="getJiraIssueTypes"
+ />
</template>
<gl-card v-else class="gl-mt-7">
<strong>{{ __('This is a Premium feature') }}</strong>
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
index bc005aa16e9..af4e9acf4ba 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlFormGroup, GlFormCheckbox, GlFormRadio } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
import { s__ } from '~/locale';
const commentDetailOptions = [
diff --git a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
index 4e2c37ac7f3..7b3a067b186 100644
--- a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
+++ b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState } from 'vuex';
import { GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
+import { mapState } from 'vuex';
import { s__ } from '~/locale';
import { defaultIntegrationLevel, overrideDropdownDescriptions } from '../constants';
diff --git a/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
index d8503910566..9472a3eeafe 100644
--- a/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
+++ b/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlModal } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/integrations/edit/components/trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
index 32878c6afa4..1bbecea05ad 100644
--- a/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
@@ -1,7 +1,7 @@
<script>
-import { mapGetters } from 'vuex';
-import { startCase } from 'lodash';
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
+import { startCase } from 'lodash';
+import { mapGetters } from 'vuex';
import { __ } from '~/locale';
const typeWithPlaceholder = {
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index 95a53f1beab..ab9bdd9ca2e 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
-import { createStore } from './store';
import { parseBoolean } from '~/lib/utils/common_utils';
import IntegrationForm from './components/integration_form.vue';
+import { createStore } from './store';
function parseBooleanInData(data) {
const result = {};
@@ -27,6 +27,7 @@ function parseDatasetToProps(data) {
cancelPath,
testPath,
resetPath,
+ vulnerabilitiesIssuetype,
...booleanAttributes
} = data;
const {
@@ -38,7 +39,9 @@ function parseDatasetToProps(data) {
mergeRequestEvents,
enableComments,
showJiraIssuesIntegration,
+ showJiraVulnerabilitiesIntegration,
enableJiraIssues,
+ enableJiraVulnerabilities,
gitlabIssuesEnabled,
} = parseBooleanInData(booleanAttributes);
@@ -59,7 +62,10 @@ function parseDatasetToProps(data) {
},
jiraIssuesProps: {
showJiraIssuesIntegration,
+ showJiraVulnerabilitiesIntegration,
initialEnableJiraIssues: enableJiraIssues,
+ initialEnableJiraVulnerabilities: enableJiraVulnerabilities,
+ initialVulnerabilitiesIssuetype: vulnerabilitiesIssuetype,
initialProjectKey: projectKey,
gitlabIssuesEnabled,
upgradePlanPath,
@@ -80,21 +86,29 @@ export default (el, defaultEl) => {
}
const props = parseDatasetToProps(el.dataset);
-
const initialState = {
defaultState: null,
customState: props,
};
-
if (defaultEl) {
initialState.defaultState = Object.freeze(parseDatasetToProps(defaultEl.dataset));
}
+ // Here, we capture the "helpHtml", so we can pass it to the Vue component
+ // to position it where ever it wants.
+ // Because this node is a _child_ of `el`, it will be removed when the Vue component is mounted,
+ // so we don't need to manually remove it.
+ const helpHtml = el.querySelector('.js-integration-help-html')?.innerHTML;
+
return new Vue({
el,
store: createStore(initialState),
render(createElement) {
- return createElement(IntegrationForm);
+ return createElement(IntegrationForm, {
+ props: {
+ helpHtml,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js
index 421917b720a..400397c050c 100644
--- a/app/assets/javascripts/integrations/edit/store/actions.js
+++ b/app/assets/javascripts/integrations/edit/store/actions.js
@@ -26,3 +26,18 @@ export const fetchResetIntegration = ({ dispatch, getters }) => {
.then(() => dispatch('receiveResetIntegrationSuccess'))
.catch(() => dispatch('receiveResetIntegrationError'));
};
+
+export const requestJiraIssueTypes = ({ commit }) => {
+ commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, '');
+ commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, true);
+};
+export const receiveJiraIssueTypesSuccess = ({ commit }, issueTypes = []) => {
+ commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, false);
+ commit(types.SET_JIRA_ISSUE_TYPES, issueTypes);
+};
+
+export const receiveJiraIssueTypesError = ({ commit }, errorMessage) => {
+ commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, false);
+ commit(types.SET_JIRA_ISSUE_TYPES, []);
+ commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, errorMessage);
+};
diff --git a/app/assets/javascripts/integrations/edit/store/mutation_types.js b/app/assets/javascripts/integrations/edit/store/mutation_types.js
index 54928148b22..c681056a515 100644
--- a/app/assets/javascripts/integrations/edit/store/mutation_types.js
+++ b/app/assets/javascripts/integrations/edit/store/mutation_types.js
@@ -3,5 +3,9 @@ export const SET_IS_SAVING = 'SET_IS_SAVING';
export const SET_IS_TESTING = 'SET_IS_TESTING';
export const SET_IS_RESETTING = 'SET_IS_RESETTING';
+export const SET_IS_LOADING_JIRA_ISSUE_TYPES = 'SET_IS_LOADING_JIRA_ISSUE_TYPES';
+export const SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE = 'SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE';
+export const SET_JIRA_ISSUE_TYPES = 'SET_JIRA_ISSUE_TYPES';
+
export const REQUEST_RESET_INTEGRATION = 'REQUEST_RESET_INTEGRATION';
export const RECEIVE_RESET_INTEGRATION_ERROR = 'RECEIVE_RESET_INTEGRATION_ERROR';
diff --git a/app/assets/javascripts/integrations/edit/store/mutations.js b/app/assets/javascripts/integrations/edit/store/mutations.js
index 826757e665b..279df1b9266 100644
--- a/app/assets/javascripts/integrations/edit/store/mutations.js
+++ b/app/assets/javascripts/integrations/edit/store/mutations.js
@@ -19,4 +19,13 @@ export default {
[types.RECEIVE_RESET_INTEGRATION_ERROR](state) {
state.isResetting = false;
},
+ [types.SET_JIRA_ISSUE_TYPES](state, jiraIssueTypes) {
+ state.jiraIssueTypes = jiraIssueTypes;
+ },
+ [types.SET_IS_LOADING_JIRA_ISSUE_TYPES](state, isLoadingJiraIssueTypes) {
+ state.isLoadingJiraIssueTypes = isLoadingJiraIssueTypes;
+ },
+ [types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE](state, errorMessage) {
+ state.loadingJiraIssueTypesErrorMessage = errorMessage;
+ },
};
diff --git a/app/assets/javascripts/integrations/edit/store/state.js b/app/assets/javascripts/integrations/edit/store/state.js
index aae3db1583f..1c0b274e4ef 100644
--- a/app/assets/javascripts/integrations/edit/store/state.js
+++ b/app/assets/javascripts/integrations/edit/store/state.js
@@ -8,5 +8,8 @@ export default ({ defaultState = null, customState = {} } = {}) => {
isSaving: false,
isTesting: false,
isResetting: false,
+ isLoadingJiraIssueTypes: false,
+ loadingJiraIssueTypesErrorMessage: '',
+ jiraIssueTypes: [],
};
};
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 861655a6a64..801cf3ed27e 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { delay } from 'lodash';
-import axios from '../lib/utils/axios_utils';
import { __, s__ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
+import axios from '../lib/utils/axios_utils';
import initForm from './edit';
import eventHub from './edit/event_hub';
@@ -33,6 +33,12 @@ export default class IntegrationSettingsForm {
eventHub.$on('saveIntegration', () => {
this.saveIntegration();
});
+ eventHub.$on('getJiraIssueTypes', () => {
+ // eslint-disable-next-line no-jquery/no-serialize
+ this.getJiraIssueTypes(this.$form.serialize());
+ });
+
+ eventHub.$emit('formInitialized');
}
saveIntegration() {
@@ -80,15 +86,58 @@ export default class IntegrationSettingsForm {
}
/**
+ * Get a list of Jira issue types for the currently configured project
+ *
+ * @param {string} formData - URL encoded string containing the form data
+ *
+ * @return {Promise}
+ */
+ getJiraIssueTypes(formData) {
+ const {
+ $store: { dispatch },
+ } = this.vue;
+
+ dispatch('requestJiraIssueTypes');
+
+ return this.fetchTestSettings(formData)
+ .then(
+ ({
+ data: {
+ issuetypes,
+ error,
+ message = s__('Integrations|Connection failed. Please check your settings.'),
+ },
+ }) => {
+ if (error || !issuetypes?.length) {
+ eventHub.$emit('validateForm');
+ throw new Error(message);
+ }
+
+ dispatch('receiveJiraIssueTypesSuccess', issuetypes);
+ },
+ )
+ .catch(({ message = __('Something went wrong on our end.') }) => {
+ dispatch('receiveJiraIssueTypesError', message);
+ });
+ }
+
+ /**
+ * Send request to the test endpoint which checks if the current config is valid
+ */
+ fetchTestSettings(formData) {
+ return axios.put(this.testEndPoint, formData);
+ }
+
+ /**
* Test Integration config
*/
testSettings(formData) {
- return axios
- .put(this.testEndPoint, formData)
+ return this.fetchTestSettings(formData)
.then(({ data }) => {
if (data.error) {
toast(`${data.message} ${data.service_response}`);
} else {
+ this.vue.$store.dispatch('receiveJiraIssueTypesSuccess', data.issuetypes);
toast(s__('Integrations|Connection successful.'));
}
})
diff --git a/app/assets/javascripts/invite_member/components/invite_member_modal.vue b/app/assets/javascripts/invite_member/components/invite_member_modal.vue
index 3df99bccdb0..144c1a2c22a 100644
--- a/app/assets/javascripts/invite_member/components/invite_member_modal.vue
+++ b/app/assets/javascripts/invite_member/components/invite_member_modal.vue
@@ -1,8 +1,9 @@
<script>
import { GlModal, GlLink } from '@gitlab/ui';
-import eventHub from '../event_hub';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, __ } from '~/locale';
import { OPEN_MODAL, MODAL_ID } from '../constants';
+import eventHub from '../event_hub';
export default {
cancelProps: {
@@ -38,7 +39,7 @@ export default {
},
methods: {
openModal() {
- this.$root.$emit('bv::show::modal', MODAL_ID);
+ this.$root.$emit(BV_SHOW_MODAL, MODAL_ID);
},
},
};
diff --git a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue b/app/assets/javascripts/invite_member/components/invite_member_trigger.vue
index 6e886e0e002..56cf1ab2fc2 100644
--- a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue
+++ b/app/assets/javascripts/invite_member/components/invite_member_trigger.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink } from '@gitlab/ui';
-import eventHub from '../event_hub';
import { OPEN_MODAL } from '../constants';
+import eventHub from '../event_hub';
export default {
components: {
diff --git a/app/assets/javascripts/invite_member/init_invite_member_modal.js b/app/assets/javascripts/invite_member/init_invite_member_modal.js
index 901e3e315ee..c292bda1931 100644
--- a/app/assets/javascripts/invite_member/init_invite_member_modal.js
+++ b/app/assets/javascripts/invite_member/init_invite_member_modal.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
+import Vue from 'vue';
import InviteMemberModal from './components/invite_member_modal.vue';
Vue.use(GlToast);
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index a92289ca8c1..f5a65882fba 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -10,10 +10,11 @@ import {
GlFormInput,
} from '@gitlab/ui';
import { partition, isString } from 'lodash';
-import eventHub from '../event_hub';
-import { s__, __, sprintf } from '~/locale';
import Api from '~/api';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
+import { s__, __, sprintf } from '~/locale';
+import eventHub from '../event_hub';
export default {
name: 'InviteMembersModal',
@@ -113,10 +114,10 @@ export default {
];
},
openModal() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
closeModal() {
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
sendInvite() {
this.submitForm();
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index 627d4ab2771..233a214013b 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -1,9 +1,9 @@
<script>
-import { debounce } from 'lodash';
import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlSprintf } from '@gitlab/ui';
+import { debounce } from 'lodash';
import { __ } from '~/locale';
-import { USER_SEARCH_DELAY } from '../constants';
import { getUsers } from '~/rest_api';
+import { USER_SEARCH_DELAY } from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index 74c374018de..3de99dcc546 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -1,7 +1,7 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import Vue from 'vue';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(GlToast);
diff --git a/app/assets/javascripts/issuable/components/issuable_by_email.vue b/app/assets/javascripts/issuable/components/issuable_by_email.vue
new file mode 100644
index 00000000000..6d063b59922
--- /dev/null
+++ b/app/assets/javascripts/issuable/components/issuable_by_email.vue
@@ -0,0 +1,169 @@
+<script>
+import {
+ GlButton,
+ GlModal,
+ GlModalDirective,
+ GlTooltipDirective,
+ GlSprintf,
+ GlLink,
+ GlFormInputGroup,
+ GlIcon,
+} from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { sprintf, __ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+export default {
+ name: 'IssuableByEmail',
+ components: {
+ GlButton,
+ GlModal,
+ GlSprintf,
+ GlLink,
+ GlFormInputGroup,
+ GlIcon,
+ ModalCopyButton,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: {
+ initialEmail: {
+ default: null,
+ },
+ issuableType: {
+ default: '',
+ },
+ emailsHelpPagePath: {
+ default: '',
+ },
+ quickActionsHelpPath: {
+ default: '',
+ },
+ markdownHelpPath: {
+ default: '',
+ },
+ resetPath: {
+ default: '',
+ },
+ },
+ data() {
+ return {
+ email: this.initialEmail,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ issuableName: this.issuableType === 'issue' ? 'issue' : 'merge request',
+ };
+ },
+ computed: {
+ mailToLink() {
+ const subject = sprintf(__('Enter the %{name} title'), {
+ name: this.issuableName,
+ });
+ const body = sprintf(__('Enter the %{name} description'), {
+ name: this.issuableName,
+ });
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `mailto:${this.email}?subject=${subject}&body=${body}`;
+ },
+ },
+ methods: {
+ async resetIncomingEmailToken() {
+ try {
+ const {
+ data: { new_address: newAddress },
+ } = await axios.put(this.resetPath);
+ this.email = newAddress;
+ } catch {
+ this.$toast.show(__('There was an error when reseting email token.'), { type: 'error' });
+ }
+ },
+ cancelHandler() {
+ this.$refs.modal.hide();
+ },
+ },
+ modalId: 'issuable-email-modal',
+};
+</script>
+
+<template>
+ <div>
+ <gl-button v-gl-modal="$options.modalId" variant="link" data-testid="issuable-email-modal-btn"
+ ><gl-sprintf :message="__('Email a new %{name} to this project')"
+ ><template #name>{{ issuableName }}</template></gl-sprintf
+ ></gl-button
+ >
+ <gl-modal ref="modal" :modal-id="$options.modalId">
+ <template #modal-title>
+ <gl-sprintf :message="__('Create new %{name} by email')">
+ <template #name>{{ issuableName }}</template>
+ </gl-sprintf>
+ </template>
+ <p>
+ <gl-sprintf
+ :message="
+ __(
+ 'You can create a new %{name} inside this project by sending an email to the following email address:',
+ )
+ "
+ >
+ <template #name>{{ issuableName }}</template>
+ </gl-sprintf>
+ </p>
+ <gl-form-input-group :value="email" readonly select-on-click class="gl-mb-4">
+ <template #append>
+ <modal-copy-button :text="email" :title="__('Copy')" :modal-id="$options.modalId" />
+ <gl-button
+ v-gl-tooltip.hover
+ :href="mailToLink"
+ :title="__('Send email')"
+ icon="mail"
+ data-testid="mail-to-btn"
+ />
+ </template>
+ </gl-form-input-group>
+ <p>
+ <gl-sprintf
+ :message="
+ __(
+ 'The subject will be used as the title of the new issue, and the message will be the description. %{quickActionsLinkStart}Quick actions%{quickActionsLinkEnd} and styling with %{markdownLinkStart}Markdown%{markdownLinkEnd} are supported.',
+ )
+ "
+ >
+ <template #quickActionsLink="{ content }">
+ <gl-link :href="quickActionsHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ <template #markdownLink="{ content }">
+ <gl-link :href="markdownHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p>
+ <gl-sprintf
+ :message="
+ __(
+ 'This is a private email address %{helpIcon} generated just for you. Anyone who gets ahold of it can create issues or merge requests as if they were you. You should %{resetLinkStart}reset it%{resetLinkEnd} if that ever happens.',
+ )
+ "
+ >
+ <template #helpIcon>
+ <gl-link :href="emailsHelpPagePath" target="_blank"
+ ><gl-icon class="gl-text-blue-600" name="question-o"
+ /></gl-link>
+ </template>
+ <template #resetLink="{ content }">
+ <gl-button
+ variant="link"
+ data-testid="incoming-email-token-reset"
+ @click="resetIncomingEmailToken"
+ >{{ content }}</gl-button
+ >
+ </template>
+ </gl-sprintf>
+ </p>
+ <template #modal-footer>
+ <gl-button category="secondary" @click="cancelHandler">{{ s__('Cancel') }}</gl-button>
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable/init_issuable_by_email.js b/app/assets/javascripts/issuable/init_issuable_by_email.js
new file mode 100644
index 00000000000..984b826234c
--- /dev/null
+++ b/app/assets/javascripts/issuable/init_issuable_by_email.js
@@ -0,0 +1,35 @@
+import { GlToast } from '@gitlab/ui';
+import Vue from 'vue';
+import IssuableByEmail from './components/issuable_by_email.vue';
+
+Vue.use(GlToast);
+
+export default () => {
+ const el = document.querySelector('.js-issueable-by-email');
+
+ if (!el) return null;
+
+ const {
+ initialEmail,
+ issuableType,
+ emailsHelpPagePath,
+ quickActionsHelpPath,
+ markdownHelpPath,
+ resetPath,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ initialEmail,
+ issuableType,
+ emailsHelpPagePath,
+ quickActionsHelpPath,
+ markdownHelpPath,
+ resetPath,
+ },
+ render(h) {
+ return h(IssuableByEmail);
+ },
+ });
+};
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index 8bb76edbd47..f507f072253 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { difference, intersection, union } from 'lodash';
-import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as Flash } from './flash';
+import axios from './lib/utils/axios_utils';
import { __ } from './locale';
export default {
@@ -50,6 +50,7 @@ export default {
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
health_status: this.form.find('input[name="update[health_status]"]').val(),
epic_id: this.form.find('input[name="update[epic_id]"]').val(),
+ sprint_id: this.form.find('input[name="update[iteration_id]"]').val(),
add_label_ids: [],
remove_label_ids: [],
},
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index b9daa16874a..ef98db5151a 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -3,11 +3,11 @@
import $ from 'jquery';
import { property } from 'lodash';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
-import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select';
-import subscriptionSelect from './subscription_select';
-import LabelsSelect from './labels_select';
import issueableEventHub from './issues_list/eventhub';
+import LabelsSelect from './labels_select';
+import MilestoneSelect from './milestone_select';
+import subscriptionSelect from './subscription_select';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -79,6 +79,16 @@ export default class IssuableBulkUpdateSidebar {
})
.catch(() => {});
}
+
+ if (IS_EE) {
+ import('ee/vue_shared/components/sidebar/iterations_dropdown_bundle')
+ .then(({ default: iterationsDropdown }) => {
+ iterationsDropdown();
+ })
+ .catch((e) => {
+ throw e;
+ });
+ }
}
setupBulkUpdateActions() {
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 6fcff90b608..a87d4f077cc 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,8 +1,8 @@
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Cookies from 'js-cookie';
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import UsersSelect from './users_select';
import { loadCSSFile } from './lib/utils/css_utils';
+import UsersSelect from './users_select';
export default class IssuableContext {
constructor(currentUser) {
diff --git a/app/assets/javascripts/issuable_create/components/issuable_form.vue b/app/assets/javascripts/issuable_create/components/issuable_form.vue
index d7b88cc7fc8..3cbd5620063 100644
--- a/app/assets/javascripts/issuable_create/components/issuable_form.vue
+++ b/app/assets/javascripts/issuable_create/components/issuable_form.vue
@@ -1,9 +1,8 @@
<script>
import { GlForm, GlFormInput } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
-
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
+import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
export default {
LabelSelectVariant: DropdownVariant,
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index a8fd7aaecdf..1b06dffbae7 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -2,12 +2,12 @@ import $ from 'jquery';
import Pikaday from 'pikaday';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import Autosave from './autosave';
-import UsersSelect from './users_select';
-import ZenMode from './zen_mode';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
+import { loadCSSFile } from './lib/utils/css_utils';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import { queryToObject, objectToQuery } from './lib/utils/url_utility';
-import { loadCSSFile } from './lib/utils/css_utils';
+import UsersSelect from './users_select';
+import ZenMode from './zen_mode';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index 4f31d26ab5d..4856f9781ce 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -1,35 +1,7 @@
-import $ from 'jquery';
-import axios from './lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from './flash';
-import { s__, __ } from './locale';
import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar';
export default class IssuableIndex {
constructor(pagePrefix) {
issuableInitBulkUpdateSidebar.init(pagePrefix);
- IssuableIndex.resetIncomingEmailToken();
- }
-
- static resetIncomingEmailToken() {
- const $resetToken = $('.incoming-email-token-reset');
-
- $resetToken.on('click', (e) => {
- e.preventDefault();
-
- $resetToken.text(s__('EmailToken|resetting...'));
-
- axios
- .put($resetToken.attr('href'))
- .then(({ data }) => {
- $('#issuable_email').val(data.new_address).focus();
-
- $resetToken.text(s__('EmailToken|reset it'));
- })
- .catch(() => {
- flash(__('There was an error when reseting email token.'));
-
- $resetToken.text(s__('EmailToken|reset it'));
- });
- });
}
}
diff --git a/app/assets/javascripts/issuable_init_bulk_update_sidebar.js b/app/assets/javascripts/issuable_init_bulk_update_sidebar.js
index da8969c80f3..179c2b83c6c 100644
--- a/app/assets/javascripts/issuable_init_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_init_bulk_update_sidebar.js
@@ -1,5 +1,5 @@
-import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
+import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
export default {
bulkUpdateSidebar: null,
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
index 583e5cb703d..39852eba71a 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -1,13 +1,13 @@
<script>
import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { getTimeago } from '~/lib/utils/datetime_utility';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
+import { __, sprintf } from '~/locale';
import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
@@ -48,17 +48,14 @@ export default {
author() {
return this.issuable.author;
},
+ webUrl() {
+ return this.issuable.gitlabWebUrl || this.issuable.webUrl;
+ },
authorId() {
return getIdFromGraphQLId(`${this.author.id}`);
},
isIssuableUrlExternal() {
- // Check if URL is relative, which means it is internal.
- if (!/^https?:\/\//g.test(this.issuable.webUrl)) {
- return false;
- }
- // In case URL is absolute, it may or may not be internal,
- // hence use `gon.gitlab_url` which is current instance domain.
- return !this.issuable.webUrl.includes(gon.gitlab_url);
+ return isExternal(this.webUrl);
},
labels() {
return this.issuable.labels?.nodes || this.issuable.labels || [];
@@ -92,6 +89,9 @@ export default {
this.hasSlotContents('status') || this.showDiscussions || this.issuable.assignees,
);
},
+ issuableNotesLink() {
+ return setUrlFragment(this.webUrl, 'notes');
+ },
},
methods: {
hasSlotContents(slotName) {
@@ -139,7 +139,13 @@ export default {
<div class="issuable-main-info">
<div data-testid="issuable-title" class="issue-title title">
<span class="issue-title-text" dir="auto">
- <gl-link :href="issuable.webUrl" v-bind="issuableTitleProps"
+ <gl-icon
+ v-if="issuable.confidential"
+ v-gl-tooltip
+ name="eye-slash"
+ :title="__('Confidential')"
+ />
+ <gl-link :href="webUrl" v-bind="issuableTitleProps"
>{{ issuable.title
}}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2"
/></gl-link>
@@ -196,12 +202,12 @@ export default {
<li
v-if="showDiscussions"
data-testid="issuable-discussions"
- class="issuable-comments gl-display-none gl-display-sm-block"
+ class="issuable-comments gl-display-none gl-sm-display-block"
>
<gl-link
v-gl-tooltip:tooltipcontainer.top
:title="__('Comments')"
- :href="`${issuable.webUrl}#notes`"
+ :href="issuableNotesLink"
:class="{ 'no-comments': !issuable.userDiscussionsCount }"
class="gl-reset-color!"
>
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
index c5475a34d3c..708e175cdb2 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
@@ -5,11 +5,10 @@ import { uniqueId } from 'lodash';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import IssuableTabs from './issuable_tabs.vue';
-import IssuableItem from './issuable_item.vue';
-import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue';
-
import { DEFAULT_SKELETON_COUNT } from '../constants';
+import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue';
+import IssuableItem from './issuable_item.vue';
+import IssuableTabs from './issuable_tabs.vue';
export default {
components: {
@@ -230,7 +229,7 @@ export default {
:initial-sort-by="initialSortBy"
:show-checkbox="showBulkEditSidebar"
:checkbox-checked="allIssuablesChecked"
- class="gl-flex-grow-1 row-content-block"
+ class="gl-flex-grow-1 gl-border-t-none row-content-block"
@checked-input="handleAllIssuablesCheckedInput"
@onFilter="$emit('filter', $event)"
@onSort="$emit('sort', $event)"
diff --git a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
index d9aab004077..57da030e22e 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
@@ -32,7 +32,10 @@ export default {
<template>
<div class="top-area">
- <gl-tabs class="nav-links mobile-separator issuable-state-filters">
+ <gl-tabs
+ class="gl-display-flex gl-flex-fill-1 gl-p-0 gl-m-0 mobile-separator issuable-state-filters"
+ nav-class="gl-border-b-0"
+ >
<gl-tab
v-for="tab in tabs"
:key="tab.id"
@@ -41,7 +44,7 @@ export default {
>
<template #title>
<span :title="tab.titleTooltip">{{ tab.title }}</span>
- <gl-badge v-if="tabCounts" variant="neutral" size="sm" class="gl-px-2 gl-py-1!">{{
+ <gl-badge v-if="tabCounts" variant="neutral" size="sm" class="gl-tab-counter-badge">{{
tabCounts[tab.name]
}}</gl-badge>
</template>
diff --git a/app/assets/javascripts/issuable_show/components/issuable_body.vue b/app/assets/javascripts/issuable_show/components/issuable_body.vue
index c084f328f42..02cf7a67727 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_body.vue
+++ b/app/assets/javascripts/issuable_show/components/issuable_body.vue
@@ -3,9 +3,9 @@ import { GlLink } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import IssuableTitle from './issuable_title.vue';
import IssuableDescription from './issuable_description.vue';
import IssuableEditForm from './issuable_edit_form.vue';
+import IssuableTitle from './issuable_title.vue';
export default {
components: {
diff --git a/app/assets/javascripts/issuable_show/components/issuable_description.vue b/app/assets/javascripts/issuable_show/components/issuable_description.vue
index 091a4be5bd8..aa7e530972f 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_description.vue
+++ b/app/assets/javascripts/issuable_show/components/issuable_description.vue
@@ -1,6 +1,6 @@
<script>
-import $ from 'jquery';
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
export default {
diff --git a/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue b/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue
index 830a740ff78..6d139541524 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue
+++ b/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue
@@ -1,6 +1,6 @@
<script>
-import $ from 'jquery';
import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import $ from 'jquery';
import Autosave from '~/autosave';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
diff --git a/app/assets/javascripts/issuable_show/components/issuable_header.vue b/app/assets/javascripts/issuable_show/components/issuable_header.vue
index 5404753631d..de17f7e7f6b 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_header.vue
+++ b/app/assets/javascripts/issuable_show/components/issuable_header.vue
@@ -2,6 +2,7 @@
import { GlIcon, GlButton, GlTooltipDirective, GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isExternal } from '~/lib/utils/url_utility';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
@@ -49,6 +50,9 @@ export default {
authorId() {
return getIdFromGraphQLId(`${this.author.id}`);
},
+ isAuthorExternal() {
+ return isExternal(this.author.webUrl);
+ },
},
mounted() {
this.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
@@ -98,7 +102,11 @@ export default {
:src="author.avatarUrl"
:label="author.name"
class="d-none d-sm-inline-flex gl-ml-1"
- />
+ >
+ <template #meta>
+ <gl-icon v-if="isAuthorExternal" name="external-link" />
+ </template>
+ </gl-avatar-labeled>
<strong class="author d-sm-none d-inline">@{{ author.username }}</strong>
</gl-avatar-link>
</div>
@@ -112,7 +120,7 @@ export default {
</div>
<div
data-testid="header-actions"
- class="detail-page-header-actions gl-display-flex gl-display-md-block"
+ class="detail-page-header-actions gl-display-flex gl-md-display-block"
>
<slot name="header-actions"></slot>
</div>
diff --git a/app/assets/javascripts/issuable_show/components/issuable_show_root.vue b/app/assets/javascripts/issuable_show/components/issuable_show_root.vue
index 2443338e8c4..240f35b74c8 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/issuable_show/components/issuable_show_root.vue
@@ -1,8 +1,8 @@
<script>
import IssuableSidebar from '~/issuable_sidebar/components/issuable_sidebar_root.vue';
-import IssuableHeader from './issuable_header.vue';
import IssuableBody from './issuable_body.vue';
+import IssuableHeader from './issuable_header.vue';
export default {
components: {
diff --git a/app/assets/javascripts/issuable_show/components/issuable_title.vue b/app/assets/javascripts/issuable_show/components/issuable_title.vue
index d3b42fd2ffb..b7ea4a010a3 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_title.vue
+++ b/app/assets/javascripts/issuable_show/components/issuable_title.vue
@@ -54,7 +54,7 @@ export default {
<template>
<div>
<div class="title-container">
- <h2 v-safe-html="issuable.titleHtml" class="title qa-title" dir="auto"></h2>
+ <h2 v-safe-html="issuable.titleHtml || issuable.title" class="title qa-title" dir="auto"></h2>
<gl-button
v-if="enableEdit"
v-gl-tooltip.bottom
diff --git a/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue b/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue
index 7cacba1cb65..8a159139af0 100644
--- a/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue
+++ b/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue
@@ -1,7 +1,7 @@
<script>
-import Cookies from 'js-cookie';
import { GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql b/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql
index fe01d2c2e78..42e646391a8 100644
--- a/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql
+++ b/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql
@@ -3,6 +3,7 @@
query getProjectIssue($iid: String!, $fullPath: ID!) {
project(fullPath: $fullPath) {
issue(iid: $iid) {
+ id
assignees {
nodes {
...Author
diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issuable_suggestions/components/app.vue
index ac5f04147d3..d0642b64e7e 100644
--- a/app/assets/javascripts/issuable_suggestions/components/app.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/app.vue
@@ -1,8 +1,8 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import Suggestion from './item.vue';
import query from '../queries/issues.query.graphql';
+import Suggestion from './item.vue';
export default {
components: {
diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issuable_suggestions/components/item.vue
index 6e265b1bf42..dea7608685a 100644
--- a/app/assets/javascripts/issuable_suggestions/components/item.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/item.vue
@@ -1,10 +1,10 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { uniqueId } from 'lodash';
import { GlLink, GlTooltip, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
import { __ } from '~/locale';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import timeago from '~/vue_shared/mixins/timeago';
export default {
diff --git a/app/assets/javascripts/issuable_suggestions/index.js b/app/assets/javascripts/issuable_suggestions/index.js
index 9949527106b..8f7f317d6b4 100644
--- a/app/assets/javascripts/issuable_suggestions/index.js
+++ b/app/assets/javascripts/issuable_suggestions/index.js
@@ -5,7 +5,7 @@ import App from './components/app.vue';
Vue.use(VueApollo);
-export default function () {
+export default function initIssuableSuggestions() {
const el = document.getElementById('js-suggestions');
const issueTitle = document.getElementById('issue_title');
const { projectPath } = el.dataset;
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 91912c684ad..3f25682ab8b 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
+import { joinPaths } from '~/lib/utils/url_utility';
+import CreateMergeRequestDropdown from './create_merge_request_dropdown';
+import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
-import { deprecatedCreateFlash as flash } from './flash';
-import CreateMergeRequestDropdown from './create_merge_request_dropdown';
-import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from './locale';
export default class Issue {
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index d569ad573a2..e70c18040b3 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -1,20 +1,20 @@
<script>
import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import Visibility from 'visibilityjs';
-import { __, s__, sprintf } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { visitUrl } from '~/lib/utils/url_utility';
import Poll from '~/lib/utils/poll';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { __, s__, sprintf } from '~/locale';
+import recaptchaModalImplementor from '~/vue_shared/mixins/recaptcha_modal_implementor';
+import { IssuableStatus, IssuableStatusText, IssuableType } from '../constants';
import eventHub from '../event_hub';
import Service from '../services/index';
import Store from '../stores';
-import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import PinnedLinks from './pinned_links.vue';
-import recaptchaModalImplementor from '~/vue_shared/mixins/recaptcha_modal_implementor';
-import { IssuableStatus, IssuableStatusText, IssuableType } from '../constants';
+import titleComponent from './title.vue';
export default {
components: {
@@ -132,6 +132,10 @@ export default {
type: String,
required: true,
},
+ projectId: {
+ type: Number,
+ required: true,
+ },
projectNamespace: {
type: String,
required: true,
@@ -419,6 +423,7 @@ export default {
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
+ :project-id="projectId"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile"
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 2a6468c783b..5416d3bebd0 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -1,11 +1,11 @@
<script>
-import $ from 'jquery';
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
+import $ from 'jquery';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import animateMixin from '../mixins/animate';
+import { s__, sprintf } from '~/locale';
import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
+import animateMixin from '../mixins/animate';
export default {
directives: {
@@ -154,6 +154,7 @@ export default {
}"
class="md"
></div>
+ <!-- eslint-disable vue/no-mutating-props -->
<textarea
v-if="descriptionText"
ref="textarea"
@@ -163,6 +164,7 @@ export default {
dir="auto"
>
</textarea>
+ <!-- eslint-enable vue/no-mutating-props -->
<recaptcha-modal v-show="showRecaptcha" :html="recaptchaHTML" @close="closeRecaptcha" />
</div>
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index 14ada5adcf6..dd378c40b46 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import updateMixin from '../mixins/update';
import eventHub from '../event_hub';
+import updateMixin from '../mixins/update';
const issuableTypes = {
issue: __('Issue'),
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 8d417e32d62..5476a1ef897 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -1,7 +1,7 @@
<script>
-import updateMixin from '../../mixins/update';
import markdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import updateMixin from '../../mixins/update';
export default {
components: {
@@ -49,6 +49,7 @@ export default {
:textarea-value="formState.description"
>
<template #textarea>
+ <!-- eslint-disable vue/no-mutating-props -->
<textarea
id="issue-description"
ref="textarea"
@@ -62,6 +63,7 @@ export default {
@keydown.ctrl.enter="updateIssuable"
>
</textarea>
+ <!-- eslint-enable vue/no-mutating-props -->
</template>
</markdown-field>
</div>
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue
index 71299381aae..dbec6f15cab 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -1,6 +1,6 @@
<script>
-import $ from 'jquery';
import { GlIcon } from '@gitlab/ui';
+import $ from 'jquery';
import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
export default {
@@ -21,6 +21,10 @@ export default {
type: String,
required: true,
},
+ projectId: {
+ type: Number,
+ required: true,
+ },
projectNamespace: {
type: String,
required: true,
@@ -35,6 +39,7 @@ export default {
// Create the editor for the template
const editor = document.querySelector('.detail-page-description .note-textarea') || {};
editor.setValue = (val) => {
+ // eslint-disable-next-line vue/no-mutating-props
this.formState.description = val;
};
editor.getValue = () => this.formState.description;
@@ -48,11 +53,12 @@ export default {
</script>
<template>
- <div class="dropdown js-issuable-selector-wrap" data-issuable-type="issue">
+ <div class="dropdown js-issuable-selector-wrap" data-issuable-type="issues">
<button
ref="toggle"
:data-namespace-path="projectNamespace"
:data-project-path="projectPath"
+ :data-project-id="projectId"
:data-data="issuableTemplatesJson"
class="dropdown-menu-toggle js-issuable-selector"
type="button"
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue
index 34eb0451d53..d331fb47077 100644
--- a/app/assets/javascripts/issue_show/components/fields/title.vue
+++ b/app/assets/javascripts/issue_show/components/fields/title.vue
@@ -15,6 +15,7 @@ export default {
<template>
<fieldset>
<label class="sr-only" for="issuable-title">{{ __('Title') }}</label>
+ <!-- eslint-disable vue/no-mutating-props -->
<input
id="issuable-title"
ref="input"
@@ -27,5 +28,6 @@ export default {
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable"
/>
+ <!-- eslint-enable vue/no-mutating-props -->
</fieldset>
</template>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index d48bf1fe7a9..b7425448052 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -1,12 +1,12 @@
<script>
import $ from 'jquery';
-import lockedWarning from './locked_warning.vue';
-import titleField from './fields/title.vue';
-import descriptionField from './fields/description.vue';
-import editActions from './edit_actions.vue';
-import descriptionTemplate from './fields/description_template.vue';
import Autosave from '~/autosave';
import eventHub from '../event_hub';
+import editActions from './edit_actions.vue';
+import descriptionField from './fields/description.vue';
+import descriptionTemplate from './fields/description_template.vue';
+import titleField from './fields/title.vue';
+import lockedWarning from './locked_warning.vue';
export default {
components: {
@@ -46,6 +46,10 @@ export default {
type: String,
required: true,
},
+ projectId: {
+ type: Number,
+ required: true,
+ },
projectNamespace: {
type: String,
required: true,
@@ -127,6 +131,7 @@ export default {
:form-state="formState"
:issuable-templates="issuableTemplates"
:project-path="projectPath"
+ :project-id="projectId"
:project-namespace="projectNamespace"
/>
</div>
diff --git a/app/assets/javascripts/issue_show/components/header_actions.vue b/app/assets/javascripts/issue_show/components/header_actions.vue
index 998f740be0e..9c3988d0469 100644
--- a/app/assets/javascripts/issue_show/components/header_actions.vue
+++ b/app/assets/javascripts/issue_show/components/header_actions.vue
@@ -193,7 +193,7 @@ export default {
<template>
<div class="detail-page-header-actions">
<gl-dropdown
- class="gl-display-block gl-display-sm-none!"
+ class="gl-display-block gl-sm-display-none!"
block
:text="dropdownText"
:loading="isToggleStateButtonLoading"
@@ -222,7 +222,7 @@ export default {
<gl-button
v-if="showToggleIssueStateButton"
- class="gl-display-none gl-display-sm-inline-flex!"
+ class="gl-display-none gl-sm-display-inline-flex!"
category="secondary"
:data-qa-selector="qaSelector"
:loading="isToggleStateButtonLoading"
@@ -233,7 +233,7 @@ export default {
</gl-button>
<gl-dropdown
- class="gl-display-none gl-display-sm-inline-flex!"
+ class="gl-display-none gl-sm-display-inline-flex!"
toggle-class="gl-border-0! gl-shadow-none!"
no-caret
right
diff --git a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
index f9f06c3ad5a..84107d3eaca 100644
--- a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue
@@ -1,13 +1,13 @@
<script>
import { GlTab, GlTabs } from '@gitlab/ui';
-import DescriptionComponent from '../description.vue';
-import HighlightBar from './highlight_bar.vue';
import createFlash from '~/flash';
+import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import { s__ } from '~/locale';
-import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import Tracking from '~/tracking';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+import DescriptionComponent from '../description.vue';
import getAlert from './graphql/queries/get_alert.graphql';
-import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
+import HighlightBar from './highlight_bar.vue';
export default {
components: {
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index b03a91716fe..806d95ca748 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlTooltipDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import animateMixin from '../mixins/animate';
import eventHub from '../event_hub';
+import animateMixin from '../mixins/animate';
export default {
components: {
diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issue_show/incident.js
index ccac38811b5..0c81ecdc843 100644
--- a/app/assets/javascripts/issue_show/incident.js
+++ b/app/assets/javascripts/issue_show/incident.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import issuableApp from './components/app.vue';
import incidentTabs from './components/incidents/incident_tabs.vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js
index 83fd1355f26..a93abbf64df 100644
--- a/app/assets/javascripts/issue_show/issue.js
+++ b/app/assets/javascripts/issue_show/issue.js
@@ -54,6 +54,7 @@ export function initIssueHeaderActions(store) {
issueType: el.dataset.issueType,
newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath,
+ projectId: el.dataset.projectId,
reportAbusePath: el.dataset.reportAbusePath,
submitAsSpamPath: el.dataset.submitAsSpamPath,
},
diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js
index d5e7d2a8807..19d1e0eebcb 100644
--- a/app/assets/javascripts/issue_show/utils/parse_data.js
+++ b/app/assets/javascripts/issue_show/utils/parse_data.js
@@ -1,5 +1,5 @@
-import * as Sentry from '~/sentry/wrapper';
import { sanitize } from '~/lib/dompurify';
+import * as Sentry from '~/sentry/wrapper';
// We currently load + parse the data from the issue app and related merge request
let cachedParsedData;
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 02b10730153..2ede0837930 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { __ } from './locale';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __ } from './locale';
export default function issueStatusSelect() {
$('.js-issue-status').each((i, el) => {
diff --git a/app/assets/javascripts/issues_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue
index 3965fd6b0c7..b7af6e098e1 100644
--- a/app/assets/javascripts/issues_list/components/issuable.vue
+++ b/app/assets/javascripts/issues_list/components/issuable.vue
@@ -5,7 +5,7 @@
*/
// TODO: need to move this component to graphql - https://gitlab.com/gitlab-org/gitlab/-/issues/221246
-import { escape, isNumber } from 'lodash';
+import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg';
import {
GlLink,
GlTooltipDirective as GlTooltip,
@@ -14,7 +14,8 @@ import {
GlIcon,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
-import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg';
+import { escape, isNumber } from 'lodash';
+import { isScopedLabel } from '~/lib/utils/common_utils';
import {
dateInWords,
formatDate,
@@ -23,13 +24,11 @@ import {
timeFor,
newDateAsLocaleTime,
} from '~/lib/utils/datetime_utility';
+import { convertToCamelCase } from '~/lib/utils/text_utility';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import { sprintf, __ } from '~/locale';
import initUserPopovers from '~/user_popovers';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-
-import { convertToCamelCase } from '~/lib/utils/text_utility';
export default {
i18n: {
@@ -414,7 +413,7 @@ export default {
v-if="meta.visible"
:key="meta.key"
v-gl-tooltip
- class="gl-display-none gl-display-sm-flex gl-align-items-center gl-ml-3"
+ class="gl-display-none gl-sm-display-flex gl-align-items-center gl-ml-3"
:class="meta.class"
:data-testid="meta.dataTestId"
:title="meta.title"
diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
index eda8bc2b61f..0b413ce0b06 100644
--- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
@@ -1,11 +1,11 @@
<script>
-import { toNumber, omit } from 'lodash';
import {
GlEmptyState,
GlPagination,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
+import { toNumber, omit } from 'lodash';
import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
@@ -14,9 +14,9 @@ import {
historyPushState,
getParameterByName,
} from '~/lib/utils/common_utils';
+import { setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initManualOrdering from '~/manual_ordering';
-import Issuable from './issuable.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import {
sortOrderMap,
@@ -26,9 +26,9 @@ import {
PAGE_SIZE_MANUAL,
LOADING_LIST_ITEMS_LENGTH,
} from '../constants';
-import { setUrlParams } from '~/lib/utils/url_utility';
import issueableEventHub from '../eventhub';
import { emptyStateHelper } from '../service_desk_helper';
+import Issuable from './issuable.vue';
export default {
LOADING_LIST_ITEMS_LENGTH,
@@ -333,15 +333,19 @@ export default {
this.fetchIssuables();
},
handleFilter(filters) {
- let search = null;
+ const searchTokens = [];
filters.forEach((filter) => {
- if (typeof filter === 'string') {
- search = filter;
+ if (filter.type === 'filtered-search-term') {
+ if (filter.value.data) {
+ searchTokens.push(filter.value.data);
+ }
}
});
- this.filters.search = search;
+ if (searchTokens.length) {
+ this.filters.search = searchTokens.join(' ');
+ }
this.page = 1;
this.refetchIssuables();
diff --git a/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue b/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue
index 61781c576c0..7396cfe27b3 100644
--- a/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue
+++ b/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue
@@ -1,14 +1,14 @@
<script>
import { GlAlert, GlLabel } from '@gitlab/ui';
import { last } from 'lodash';
-import { n__ } from '~/locale';
-import getIssuesListDetailsQuery from '../queries/get_issues_list_details.query.graphql';
import {
calculateJiraImportLabel,
isInProgress,
setFinishedAlertHideMap,
shouldShowFinishedAlert,
} from '~/jira_import/utils/jira_import_utils';
+import { n__ } from '~/locale';
+import getIssuesListDetailsQuery from '../queries/get_issues_list_details.query.graphql';
export default {
name: 'JiraIssuesList',
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index 42e97766b95..5c3910955bc 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -2,8 +2,8 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import JiraIssuesListRoot from './components/jira_issues_list_root.vue';
import IssuablesListApp from './components/issuables_list_app.vue';
+import JiraIssuesListRoot from './components/jira_issues_list_root.vue';
function mountJiraIssuesListApp() {
const el = document.querySelector('.js-projects-issues-root');
diff --git a/app/assets/javascripts/jira_connect/api.js b/app/assets/javascripts/jira_connect/api.js
index d689a2d1962..d78aba0a3f7 100644
--- a/app/assets/javascripts/jira_connect/api.js
+++ b/app/assets/javascripts/jira_connect/api.js
@@ -1,7 +1,23 @@
import axios from 'axios';
-const getJwt = async () => {
- return AP.context.getToken();
+export const getJwt = () => {
+ return new Promise((resolve) => {
+ AP.context.getToken((token) => {
+ resolve(token);
+ });
+ });
+};
+
+export const getLocation = () => {
+ return new Promise((resolve) => {
+ if (typeof AP.getLocation !== 'function') {
+ resolve();
+ }
+
+ AP.getLocation((location) => {
+ resolve(location);
+ });
+ });
};
export const addSubscription = async (addPath, namespace) => {
diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue
index f5bf30f4488..a4ba86dc6a1 100644
--- a/app/assets/javascripts/jira_connect/components/app.vue
+++ b/app/assets/javascripts/jira_connect/components/app.vue
@@ -1,8 +1,10 @@
<script>
-import { mapState } from 'vuex';
import { GlAlert, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { getLocation } from '~/jira_connect/api';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
import GroupsList from './groups_list.vue';
export default {
@@ -17,17 +19,42 @@ export default {
GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
+ inject: {
+ usersPath: {
+ default: '',
+ },
+ },
+ data() {
+ return {
+ location: '',
+ };
+ },
computed: {
...mapState(['errorMessage']),
showNewUI() {
return this.glFeatures.newJiraConnectUi;
},
+ usersPathWithReturnTo() {
+ if (this.location) {
+ return `${this.usersPath}?return_to=${this.location}`;
+ }
+
+ return this.usersPath;
+ },
},
modal: {
cancelProps: {
text: __('Cancel'),
},
},
+ created() {
+ this.setLocation();
+ },
+ methods: {
+ async setLocation() {
+ this.location = await getLocation();
+ },
+ },
};
</script>
@@ -37,27 +64,40 @@ export default {
{{ errorMessage }}
</gl-alert>
- <h1>GitLab for Jira Configuration</h1>
+ <h2>{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
<div
v-if="showNewUI"
- class="gl-display-flex gl-justify-content-space-between gl-my-5 gl-pb-4 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
+ class="gl-display-flex gl-justify-content-space-between gl-my-7 gl-pb-4 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
>
- <h3 data-testid="new-jira-connect-ui-heading">{{ s__('Integrations|Linked namespaces') }}</h3>
+ <h5 class="gl-align-self-center gl-mb-0" data-testid="new-jira-connect-ui-heading">
+ {{ s__('Integrations|Linked namespaces') }}
+ </h5>
<gl-button
- v-gl-modal-directive="'add-namespace-modal'"
+ v-if="usersPath"
category="primary"
variant="info"
class="gl-align-self-center"
- >{{ s__('Integrations|Add namespace') }}</gl-button
- >
- <gl-modal
- modal-id="add-namespace-modal"
- :title="s__('Integrations|Link namespaces')"
- :action-cancel="$options.modal.cancelProps"
+ :href="usersPathWithReturnTo"
+ target="_blank"
+ >{{ s__('Integrations|Sign in to add namespaces') }}</gl-button
>
- <groups-list />
- </gl-modal>
+ <template v-else>
+ <gl-button
+ v-gl-modal-directive="'add-namespace-modal'"
+ category="primary"
+ variant="info"
+ class="gl-align-self-center"
+ >{{ s__('Integrations|Add namespace') }}</gl-button
+ >
+ <gl-modal
+ modal-id="add-namespace-modal"
+ :title="s__('Integrations|Link namespaces')"
+ :action-cancel="$options.modal.cancelProps"
+ >
+ <groups-list />
+ </gl-modal>
+ </template>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/components/groups_list.vue b/app/assets/javascripts/jira_connect/components/groups_list.vue
index eeddd32addc..69f2903388c 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list.vue
+++ b/app/assets/javascripts/jira_connect/components/groups_list.vue
@@ -1,9 +1,9 @@
<script>
-import { GlTabs, GlTab, GlLoadingIcon, GlPagination } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { GlTabs, GlTab, GlLoadingIcon, GlPagination, GlAlert } from '@gitlab/ui';
import { fetchGroups } from '~/jira_connect/api';
import { defaultPerPage } from '~/jira_connect/constants';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
import GroupsListItem from './groups_list_item.vue';
export default {
@@ -12,6 +12,7 @@ export default {
GlTab,
GlLoadingIcon,
GlPagination,
+ GlAlert,
GroupsListItem,
},
inject: {
@@ -26,6 +27,7 @@ export default {
page: 1,
perPage: defaultPerPage,
totalItems: 0,
+ errorMessage: null,
};
},
mounted() {
@@ -46,8 +48,7 @@ export default {
this.groups = response.data;
})
.catch(() => {
- // eslint-disable-next-line no-alert
- alert(s__('Integrations|Failed to load namespaces. Please try again.'));
+ this.errorMessage = s__('Integrations|Failed to load namespaces. Please try again.');
})
.finally(() => {
this.isLoading = false;
@@ -58,31 +59,42 @@ export default {
</script>
<template>
- <gl-tabs>
- <gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
- <gl-loading-icon v-if="isLoading" size="md" />
- <div v-else-if="groups.length === 0" class="gl-text-center">
- <h5>{{ s__('Integrations|No available namespaces.') }}</h5>
- <p class="gl-mt-5">
- {{
- s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
- }}
- </p>
- </div>
- <ul v-else class="gl-list-style-none gl-pl-0">
- <groups-list-item v-for="group in groups" :key="group.id" :group="group" />
- </ul>
+ <div>
+ <gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" @dismiss="errorMessage = null">
+ {{ errorMessage }}
+ </gl-alert>
+
+ <gl-tabs>
+ <gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
+ <gl-loading-icon v-if="isLoading" size="md" />
+ <div v-else-if="groups.length === 0" class="gl-text-center">
+ <h5>{{ s__('Integrations|No available namespaces.') }}</h5>
+ <p class="gl-mt-5">
+ {{
+ s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
+ }}
+ </p>
+ </div>
+ <ul v-else class="gl-list-style-none gl-pl-0">
+ <groups-list-item
+ v-for="group in groups"
+ :key="group.id"
+ :group="group"
+ @error="errorMessage = $event"
+ />
+ </ul>
- <div class="gl-display-flex gl-justify-content-center gl-mt-5">
- <gl-pagination
- v-if="totalItems > perPage && groups.length > 0"
- v-model="page"
- class="gl-mb-0"
- :per-page="perPage"
- :total-items="totalItems"
- @input="loadGroups"
- />
- </div>
- </gl-tab>
- </gl-tabs>
+ <div class="gl-display-flex gl-justify-content-center gl-mt-5">
+ <gl-pagination
+ v-if="totalItems > perPage && groups.length > 0"
+ v-model="page"
+ class="gl-mb-0"
+ :per-page="perPage"
+ :total-items="totalItems"
+ @input="loadGroups"
+ />
+ </div>
+ </gl-tab>
+ </gl-tabs>
+ </div>
</template>
diff --git a/app/assets/javascripts/jira_connect/components/groups_list_item.vue b/app/assets/javascripts/jira_connect/components/groups_list_item.vue
index 15e37ab3cb0..69b09ab0a21 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list_item.vue
+++ b/app/assets/javascripts/jira_connect/components/groups_list_item.vue
@@ -1,10 +1,18 @@
<script>
-import { GlIcon, GlAvatar } from '@gitlab/ui';
+import { GlAvatar, GlButton, GlIcon } from '@gitlab/ui';
+import { addSubscription } from '~/jira_connect/api';
+import { s__ } from '~/locale';
export default {
components: {
- GlIcon,
GlAvatar,
+ GlButton,
+ GlIcon,
+ },
+ inject: {
+ subscriptionsPath: {
+ default: '',
+ },
},
props: {
group: {
@@ -12,6 +20,31 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ onClick() {
+ this.isLoading = true;
+
+ addSubscription(this.subscriptionsPath, this.group.full_path)
+ .then(() => {
+ AP.navigator.reload();
+ })
+ .catch((error) => {
+ this.$emit(
+ 'error',
+ error?.response?.data?.error ||
+ s__('Integrations|Failed to link namespace. Please try again.'),
+ );
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ },
};
</script>
@@ -19,7 +52,7 @@ export default {
<li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200">
<div class="gl-display-flex gl-align-items-center gl-py-3">
<gl-icon name="folder-o" class="gl-mr-3" />
- <div class="gl-display-none gl-flex-shrink-0 gl-display-sm-flex gl-mr-3">
+ <div class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3">
<gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatar_url" />
</div>
<div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
@@ -36,6 +69,14 @@ export default {
<p class="gl-mt-2! gl-mb-0 gl-text-gray-600" v-text="group.description"></p>
</div>
</div>
+
+ <gl-button
+ category="secondary"
+ variant="success"
+ :loading="isLoading"
+ @click.prevent="onClick"
+ >{{ __('Link') }}</gl-button
+ >
</div>
</div>
</li>
diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js
index dc2a77f4e0c..7191fce3c33 100644
--- a/app/assets/javascripts/jira_connect/index.js
+++ b/app/assets/javascripts/jira_connect/index.js
@@ -1,70 +1,73 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import $ from 'jquery';
import setConfigs from '@gitlab/ui/dist/config';
-import Translate from '~/vue_shared/translate';
+import Vue from 'vue';
+import { addSubscription, removeSubscription, getLocation } from '~/jira_connect/api';
import GlFeatureFlagsPlugin from '~/vue_shared/gl_feature_flags_plugin';
+import Translate from '~/vue_shared/translate';
import JiraConnectApp from './components/app.vue';
-import { addSubscription, removeSubscription } from '~/jira_connect/api';
import createStore from './store';
import { SET_ERROR_MESSAGE } from './store/mutation_types';
-Vue.use(Vuex);
-
const store = createStore();
-/**
- * Initialize form handlers for the Jira Connect app
- */
-const initJiraFormHandlers = () => {
- const reqComplete = () => {
- AP.navigator.reload();
- };
-
- const reqFailed = (res, fallbackErrorMessage) => {
- const { error = fallbackErrorMessage } = res || {};
-
- store.commit(SET_ERROR_MESSAGE, error);
- };
-
- if (typeof AP.getLocation === 'function') {
- AP.getLocation((location) => {
- $('.js-jira-connect-sign-in').each(function updateSignInLink() {
- const updatedLink = `${$(this).attr('href')}?return_to=${location}`;
- $(this).attr('href', updatedLink);
- });
- });
- }
+const reqComplete = () => {
+ AP.navigator.reload();
+};
- $('#add-subscription-form').on('submit', function onAddSubscriptionForm(e) {
- const addPath = $(this).attr('action');
- const namespace = $('#namespace-input').val();
+const reqFailed = (res, fallbackErrorMessage) => {
+ const { error = fallbackErrorMessage } = res || {};
- e.preventDefault();
+ store.commit(SET_ERROR_MESSAGE, error);
+};
- addSubscription(addPath, namespace)
- .then(reqComplete)
- .catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.'));
+const updateSignInLinks = async () => {
+ const location = await getLocation();
+ Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
+ const updatedLink = `${el.getAttribute('href')}?return_to=${location}`;
+ el.setAttribute('href', updatedLink);
});
+};
+
+const initRemoveSubscriptionButtonHandlers = () => {
+ Array.from(document.querySelectorAll('.js-jira-connect-remove-subscription')).forEach((el) => {
+ el.addEventListener('click', function onRemoveSubscriptionClick(e) {
+ e.preventDefault();
+
+ const removePath = e.target.getAttribute('href');
+ removeSubscription(removePath)
+ .then(reqComplete)
+ .catch((err) =>
+ reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'),
+ );
+ });
+ });
+};
+
+const initAddSubscriptionFormHandler = () => {
+ const formEl = document.querySelector('#add-subscription-form');
+ if (!formEl) {
+ return;
+ }
- $('.remove-subscription').on('click', function onRemoveSubscriptionClick(e) {
- const removePath = $(this).attr('href');
+ formEl.addEventListener('submit', function onAddSubscriptionForm(e) {
e.preventDefault();
- removeSubscription(removePath)
+ const addPath = e.target.getAttribute('action');
+ const namespace = (e.target.querySelector('#namespace-input') || {}).value;
+
+ addSubscription(addPath, namespace)
.then(reqComplete)
- .catch((err) =>
- reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'),
- );
+ .catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.'));
});
};
-function initJiraConnect() {
- const el = document.querySelector('.js-jira-connect-app');
+export async function initJiraConnect() {
+ initAddSubscriptionFormHandler();
+ initRemoveSubscriptionButtonHandlers();
- initJiraFormHandlers();
+ await updateSignInLinks();
+ const el = document.querySelector('.js-jira-connect-app');
if (!el) {
return null;
}
@@ -73,13 +76,15 @@ function initJiraConnect() {
Vue.use(Translate);
Vue.use(GlFeatureFlagsPlugin);
- const { groupsPath } = el.dataset;
+ const { groupsPath, subscriptionsPath, usersPath } = el.dataset;
return new Vue({
el,
store,
provide: {
groupsPath,
+ subscriptionsPath,
+ usersPath,
},
render(createElement) {
return createElement(JiraConnectApp);
diff --git a/app/assets/javascripts/jira_connect/store/index.js b/app/assets/javascripts/jira_connect/store/index.js
index aa7e14269a4..de830e3891a 100644
--- a/app/assets/javascripts/jira_connect/store/index.js
+++ b/app/assets/javascripts/jira_connect/store/index.js
@@ -1,9 +1,12 @@
+import Vue from 'vue';
import Vuex from 'vuex';
import mutations from './mutations';
import state from './state';
+Vue.use(Vuex);
+
export default () =>
new Vuex.Store({
- state,
mutations,
+ state,
});
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index ab475c3c85a..6f2fb41ca15 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -15,10 +15,11 @@ import {
GlTable,
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import getJiraUserMappingMutation from '../queries/get_jira_user_mapping.mutation.graphql';
import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql';
+import searchProjectMembersQuery from '../queries/search_project_members.query.graphql';
import { addInProgressImportToStore } from '../utils/cache_update';
import {
debounceWait,
@@ -155,19 +156,23 @@ export default {
});
},
searchUsers() {
- const params = {
- active: true,
- project_id: this.projectId,
- search: this.searchTerm,
- };
-
this.isFetching = true;
- return axios
- .get('/-/autocomplete/users.json', { params })
+ return this.$apollo
+ .query({
+ query: searchProjectMembersQuery,
+ variables: {
+ fullPath: this.projectPath,
+ search: this.searchTerm,
+ },
+ })
.then(({ data }) => {
- this.users = data;
- return data;
+ this.users =
+ data?.project?.projectMembers?.nodes?.map(({ user }) => ({
+ ...user,
+ id: getIdFromGraphQLId(user.id),
+ })) || [];
+ return this.users;
})
.finally(() => {
this.isFetching = false;
diff --git a/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql b/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
new file mode 100644
index 00000000000..06f119e75ed
--- /dev/null
+++ b/app/assets/javascripts/jira_import/queries/search_project_members.query.graphql
@@ -0,0 +1,13 @@
+query searchProjectMembers($fullPath: ID!, $search: String) {
+ project(fullPath: $fullPath) {
+ projectMembers(search: $search) {
+ nodes {
+ user {
+ id
+ name
+ username
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 2850a8e86fd..0f34926f689 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -1,13 +1,15 @@
<script>
-import { GlIcon, GlLink } from '@gitlab/ui';
+import { GlButton, GlButtonGroup, GlIcon, GlLink } from '@gitlab/ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
- TimeagoTooltip,
+ GlButton,
+ GlButtonGroup,
GlIcon,
GlLink,
+ TimeagoTooltip,
},
mixins: [timeagoMixin],
props: {
@@ -36,7 +38,7 @@ export default {
</script>
<template>
<div class="block">
- <div class="title font-weight-bold">{{ s__('Job|Job artifacts') }}</div>
+ <div class="title gl-font-weight-bold">{{ s__('Job|Job artifacts') }}</div>
<p
v-if="isExpired || willExpire"
class="build-detail-row"
@@ -61,32 +63,29 @@ export default {
)
}}</span>
</p>
- <div class="btn-group d-flex gl-mt-3" role="group">
- <gl-link
+ <gl-button-group class="gl-display-flex gl-mt-3">
+ <gl-button
v-if="artifact.keep_path"
:href="artifact.keep_path"
- class="btn btn-sm btn-default"
data-method="post"
data-testid="keep-artifacts"
- >{{ s__('Job|Keep') }}</gl-link
+ >{{ s__('Job|Keep') }}</gl-button
>
- <gl-link
+ <gl-button
v-if="artifact.download_path"
:href="artifact.download_path"
- class="btn btn-sm btn-default"
- download
rel="nofollow"
data-testid="download-artifacts"
- >{{ s__('Job|Download') }}</gl-link
+ download
+ >{{ s__('Job|Download') }}</gl-button
>
- <gl-link
+ <gl-button
v-if="artifact.browse_path"
:href="artifact.browse_path"
- class="btn btn-sm btn-default"
data-testid="browse-artifacts"
data-qa-selector="browse_artifacts_button"
- >{{ s__('Job|Browse') }}</gl-link
+ >{{ s__('Job|Browse') }}</gl-button
>
- </div>
+ </gl-button-group>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index 0ee8cd6c5ad..35b16d73cc7 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -85,7 +85,7 @@ export default {
<gl-link
:href="action.path"
:data-method="action.method"
- class="btn btn-primary"
+ class="btn gl-button btn-confirm gl-text-decoration-none!"
data-testid="job-empty-state-action"
>{{ action.button_title }}</gl-link
>
diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue
index ec7868d9235..9d451f94e8a 100644
--- a/app/assets/javascripts/jobs/components/environments_block.vue
+++ b/app/assets/javascripts/jobs/components/environments_block.vue
@@ -1,6 +1,6 @@
<script>
-import { isEmpty } from 'lodash';
import { GlSprintf, GlLink } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { __ } from '../../locale';
@@ -201,9 +201,7 @@ export default {
/>
<template v-else>{{ clusterNameOrLink.name }}</template>
</template>
- <template #kubernetesNamespace>
- <template>{{ kubernetesNamespace }}</template>
- </template>
+ <template #kubernetesNamespace>{{ kubernetesNamespace }}</template>
<template #deploymentLink>
<gl-link
:href="deploymentLink.path"
diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue
index a6d1b41c275..a815689659e 100644
--- a/app/assets/javascripts/jobs/components/erased_block.vue
+++ b/app/assets/javascripts/jobs/components/erased_block.vue
@@ -1,12 +1,14 @@
<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import { GlLink } from '@gitlab/ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- TimeagoTooltip,
+ GlAlert,
GlLink,
+ GlSprintf,
+ TimeagoTooltip,
},
props: {
user: {
@@ -27,17 +29,21 @@ export default {
};
</script>
<template>
- <div class="gl-mt-3 js-build-erased">
- <div class="erased alert alert-warning">
+ <div class="gl-mt-3">
+ <gl-alert variant="warning" :dismissible="false">
<template v-if="isErasedByUser">
- {{ s__('Job|Job has been erased by') }}
- <gl-link :href="user.web_url"> {{ user.username }} </gl-link>
+ <gl-sprintf :message="s__('Job|Job has been erased by %{userLink}')">
+ <template #userLink>
+ <gl-link :href="user.web_url" target="_blank">{{ user.username }}</gl-link>
+ </template>
+ </gl-sprintf>
</template>
+
<template v-else>
{{ s__('Job|Job has been erased') }}
</template>
<timeago-tooltip :time="erasedAt" />
- </div>
+ </gl-alert>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index b0ba6ce52d1..91ab68d5f39 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -1,21 +1,20 @@
<script>
-import { throttle, isEmpty } from 'lodash';
-import { mapGetters, mapState, mapActions } from 'vuex';
import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { throttle, isEmpty } from 'lodash';
+import { mapGetters, mapState, mapActions } from 'vuex';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
-import { polyfillSticky } from '~/lib/utils/sticky';
+import { sprintf } from '~/locale';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
+import delayedJobMixin from '../mixins/delayed_job_mixin';
import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue';
import LogTopBar from './job_log_controllers.vue';
+import Log from './log/log.vue';
+import Sidebar from './sidebar.vue';
import StuckBlock from './stuck_block.vue';
import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue';
-import Sidebar from './sidebar.vue';
-import { sprintf } from '~/locale';
-import delayedJobMixin from '../mixins/delayed_job_mixin';
-import Log from './log/log.vue';
export default {
name: 'JobPageApp',
@@ -54,11 +53,6 @@ export default {
required: false,
default: null,
},
- runnerHelpUrl: {
- type: String,
- required: false,
- default: null,
- },
deploymentHelpUrl: {
type: String,
required: false,
@@ -140,14 +134,6 @@ export default {
this.fetchJobsForStage(defaultStage);
}
}
-
- if (newVal.archived) {
- this.$nextTick(() => {
- if (this.$refs.sticky) {
- polyfillSticky(this.$refs.sticky);
- }
- });
- }
},
},
created() {
@@ -250,7 +236,6 @@ export default {
v-if="shouldRenderSharedRunnerLimitWarning"
:quota-used="job.runners.quota.used"
:quota-limit="job.runners.quota.limit"
- :runners-path="runnerHelpUrl"
:project-path="projectPath"
:subscriptions-more-minutes-url="subscriptionsMoreMinutesUrl"
/>
@@ -271,7 +256,6 @@ export default {
<div
v-if="job.archived"
- ref="sticky"
class="gl-mt-3 archived-job"
:class="{ 'sticky-top border-bottom-0': hasTrace }"
data-testid="archived-job"
@@ -330,7 +314,6 @@ export default {
'right-sidebar-collapsed': !isSidebarOpen,
}"
:artifact-help-url="artifactHelpUrl"
- :runner-help-url="runnerHelpUrl"
data-testid="job-sidebar"
/>
</div>
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index 6b61dc5902b..e68368919ab 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -1,8 +1,8 @@
<script>
import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import { sprintf } from '~/locale';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
export default {
components: {
@@ -51,7 +51,7 @@ export default {
v-gl-tooltip
:href="job.status.details_path"
:title="tooltipText"
- class="js-job-link d-flex"
+ class="js-job-link gl-display-flex gl-align-items-center"
>
<gl-icon
v-if="isActive"
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 7e7d9a0549b..fbdbfddff56 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -1,7 +1,6 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
-import { polyfillSticky } from '~/lib/utils/sticky';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __, sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg';
@@ -54,9 +53,6 @@ export default {
});
},
},
- mounted() {
- polyfillSticky(this.$el);
- },
methods: {
handleScrollToTop() {
this.$emit('scrollJobLogTop');
diff --git a/app/assets/javascripts/jobs/components/log/duration_badge.vue b/app/assets/javascripts/jobs/components/log/duration_badge.vue
index 8e5dcdcc902..54b76fd9edd 100644
--- a/app/assets/javascripts/jobs/components/log/duration_badge.vue
+++ b/app/assets/javascripts/jobs/components/log/duration_badge.vue
@@ -1,5 +1,10 @@
<script>
+import { GlBadge } from '@gitlab/ui';
+
export default {
+ components: {
+ GlBadge,
+ },
props: {
duration: {
type: String,
@@ -9,7 +14,7 @@ export default {
};
</script>
<template>
- <div class="log-duration-badge rounded align-self-start px-2 ml-2 flex-shrink-0 ws-normal">
+ <gl-badge>
{{ duration }}
- </div>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/jobs/components/log/line_header.vue
index 4c1c00cb2a7..3bb1f58573c 100644
--- a/app/assets/javascripts/jobs/components/log/line_header.vue
+++ b/app/assets/javascripts/jobs/components/log/line_header.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon } from '@gitlab/ui';
-import LineNumber from './line_number.vue';
import DurationBadge from './duration_badge.vue';
+import LineNumber from './line_number.vue';
export default {
components: {
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
index 24276cbe60a..a1f4f7abb77 100644
--- a/app/assets/javascripts/jobs/components/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlButton } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
-import { GlButton } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 0789bb54f0f..f63fe72a71a 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -1,17 +1,17 @@
<script>
+import { GlButton, GlIcon, GlLink } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlButton, GlIcon, GlLink } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import { JOB_SIDEBAR } from '../constants';
import ArtifactsBlock from './artifacts_block.vue';
-import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
-import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue';
-import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue';
-import StagesDropdown from './stages_dropdown.vue';
+import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue';
+import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
import JobsContainer from './jobs_container.vue';
import JobSidebarDetailsContainer from './sidebar_job_details_container.vue';
-import { JOB_SIDEBAR } from '../constants';
+import StagesDropdown from './stages_dropdown.vue';
+import TriggerBlock from './trigger_block.vue';
export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
@@ -41,19 +41,14 @@ export default {
required: false,
default: '',
},
- runnerHelpUrl: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
...mapState(['job', 'stages', 'jobs', 'selectedStage']),
retryButtonClass() {
- let className = 'btn btn-retry';
+ let className = 'btn gl-button gl-text-decoration-none!';
className +=
- this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
+ this.job.status && this.job.recoverable ? ' btn-confirm' : ' btn-confirm-secondary';
return className;
},
hasArtifact() {
@@ -99,7 +94,7 @@ export default {
<gl-link
v-if="job.cancel_path"
:href="job.cancel_path"
- class="btn btn-default"
+ class="btn gl-button btn-default gl-text-decoration-none!"
data-method="post"
data-testid="cancel-button"
rel="nofollow"
@@ -110,7 +105,7 @@ export default {
<gl-button
:aria-label="$options.i18n.toggleSidebar"
category="tertiary"
- class="gl-display-md-none gl-ml-2 js-sidebar-build-toggle"
+ class="gl-md-display-none gl-ml-2 js-sidebar-build-toggle"
icon="chevron-double-lg-right"
@click="toggleSidebar"
/>
@@ -120,7 +115,7 @@ export default {
<gl-link
v-if="job.new_issue_path"
:href="job.new_issue_path"
- class="btn btn-success btn-inverted float-left mr-2"
+ class="btn gl-button btn-success-secondary float-left mr-2 gl-text-decoration-none!"
data-testid="job-new-issue"
>{{ $options.i18n.newIssue }}
</gl-link>
@@ -135,7 +130,7 @@ export default {
<gl-icon :size="14" name="external-link" />
</gl-link>
</div>
- <job-sidebar-details-container :runner-help-url="runnerHelpUrl" />
+ <job-sidebar-details-container />
<artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" />
<trigger-block v-if="hasTriggers" :trigger="job.trigger" />
<commit-block
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index 7d541f93bad..05567328660 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -34,12 +34,12 @@ export default {
};
</script>
<template>
- <p class="build-detail-row">
- <span v-if="hasTitle" class="font-weight-bold">{{ title }}:</span> {{ value }}
- <span v-if="hasHelpURL" class="help-button float-right">
- <gl-link :href="helpUrl" target="_blank" rel="noopener noreferrer nofollow">
- <gl-icon name="question-o" />
- </gl-link>
- </span>
+ <p class="gl-display-flex gl-justify-content-space-between gl-mb-2">
+ <span v-if="hasTitle"
+ ><b>{{ title }}:</b> {{ value }}</span
+ >
+ <gl-link v-if="hasHelpURL" :href="helpUrl" target="_blank">
+ <gl-icon name="question-o" />
+ </gl-link>
</p>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
index 8ad1008278e..62cd30fb320 100644
--- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
@@ -1,9 +1,10 @@
<script>
import { mapState } from 'vuex';
-import DetailRow from './sidebar_detail_row.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
+import DetailRow from './sidebar_detail_row.vue';
export default {
name: 'JobSidebarDetailsContainer',
@@ -11,13 +12,6 @@ export default {
DetailRow,
},
mixins: [timeagoMixin],
- props: {
- runnerHelpUrl: {
- type: String,
- required: false,
- default: '',
- },
- },
computed: {
...mapState(['job']),
coverage() {
@@ -51,6 +45,11 @@ export default {
queued() {
return timeIntervalInWords(this.job.queued);
},
+ runnerHelpUrl() {
+ return helpPagePath('ci/runners/README.html', {
+ anchor: 'set-maximum-job-timeout-for-a-runner',
+ });
+ },
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
@@ -96,7 +95,12 @@ export default {
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
<span class="font-weight-bold">{{ __('Tags:') }}</span>
- <span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{ tag }}</span>
+ <span
+ v-for="(tag, i) in job.tags"
+ :key="i"
+ class="badge badge-pill badge-primary gl-badge sm"
+ >{{ tag }}</span
+ >
</p>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index 7d4fe0a0680..64c4031b002 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -1,6 +1,6 @@
<script>
-import { isEmpty } from 'lodash';
import { GlLink, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
export default {
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index 8e8202246a2..abd0c13702a 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert, GlBadge, GlLink } from '@gitlab/ui';
-import { s__ } from '../../locale';
+import { s__ } from '~/locale';
/**
* Renders Stuck Runners block for job's view.
*/
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 1d46dd8cea4..f6b98777011 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -1,12 +1,31 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
-const HIDDEN_VALUE = '••••••';
+const DEFAULT_TD_CLASSES = 'gl-w-half gl-font-sm! gl-border-gray-200!';
+const DEFAULT_TH_CLASSES =
+ 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1!';
export default {
+ fields: [
+ {
+ key: 'key',
+ label: __('Key'),
+ tdAttr: { 'data-testid': 'trigger-build-key' },
+ tdClass: DEFAULT_TD_CLASSES,
+ thClass: DEFAULT_TH_CLASSES,
+ },
+ {
+ key: 'value',
+ label: __('Value'),
+ tdAttr: { 'data-testid': 'trigger-build-value' },
+ tdClass: DEFAULT_TD_CLASSES,
+ thClass: DEFAULT_TH_CLASSES,
+ },
+ ],
components: {
GlButton,
+ GlTable,
},
props: {
trigger: {
@@ -21,7 +40,7 @@ export default {
},
computed: {
hasVariables() {
- return this.trigger.variables && this.trigger.variables.length > 0;
+ return this.trigger.variables.length > 0;
},
getToggleButtonText() {
return this.showVariableValues ? __('Hide values') : __('Reveal values');
@@ -35,45 +54,41 @@ export default {
this.showVariableValues = !this.showVariableValues;
},
getDisplayValue(value) {
- return this.showVariableValues ? value : HIDDEN_VALUE;
+ return this.showVariableValues ? value : '••••••';
},
},
};
</script>
<template>
- <div class="build-widget block">
+ <div class="block">
<p
v-if="trigger.short_token"
- class="js-short-token"
:class="{ 'gl-mb-2': hasVariables, 'gl-mb-0': !hasVariables }"
+ data-testid="trigger-short-token"
>
- <span class="font-weight-bold">{{ __('Trigger token:') }}</span> {{ trigger.short_token }}
+ <span class="gl-font-weight-bold">{{ __('Trigger token:') }}</span> {{ trigger.short_token }}
</p>
<template v-if="hasVariables">
- <p class="trigger-variables-btn-container d-flex">
- <span class="font-weight-bold">{{ __('Trigger variables:') }}</span>
+ <p class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <span class="gl-font-weight-bold">{{ __('Trigger variables:') }}</span>
<gl-button
v-if="hasValues"
- class="group js-reveal-variables trigger-variables-btn"
+ class="gl-mt-2"
size="small"
+ data-testid="trigger-reveal-values-button"
@click="toggleValues"
>{{ getToggleButtonText }}</gl-button
>
</p>
- <table class="js-build-variables trigger-build-variables">
- <tr v-for="(variable, index) in trigger.variables" :key="`${variable.key}-${index}`">
- <td class="js-build-variable trigger-build-variable trigger-variables-table-cell">
- {{ variable.key }}
- </td>
- <td class="js-build-value trigger-build-value trigger-variables-table-cell">
- {{ getDisplayValue(variable.value) }}
- </td>
- </tr>
- </table>
+ <gl-table :items="trigger.variables" :fields="$options.fields" small bordered>
+ <template #cell(value)="data">
+ {{ getDisplayValue(data.value) }}
+ </template>
+ </gl-table>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 1ad6292a030..3e00056ee81 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -13,7 +13,6 @@ export default () => {
const {
artifactHelpUrl,
deploymentHelpUrl,
- runnerHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
@@ -39,7 +38,6 @@ export default () => {
props: {
artifactHelpUrl,
deploymentHelpUrl,
- runnerHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index e76a3693db9..c89aeada69d 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -1,10 +1,9 @@
import Visibility from 'visibilityjs';
-import * as types from './mutation_types';
+import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import Poll from '~/lib/utils/poll';
import { setFaviconOverlay, resetFavicon } from '~/lib/utils/favicon';
-import { deprecatedCreateFlash as flash } from '~/flash';
-import { __ } from '~/locale';
+import httpStatusCodes from '~/lib/utils/http_status';
+import Poll from '~/lib/utils/poll';
import {
canScroll,
isScrolledToBottom,
@@ -13,7 +12,8 @@ import {
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { __ } from '~/locale';
+import * as types from './mutation_types';
export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
dispatch('setJobEndpoint', endpoint);
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 30a4a247dc4..930a225857d 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -1,7 +1,7 @@
import { isEmpty, isString } from 'lodash';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
-export const headerTime = (state) => (state.job.started ? state.job.started : state.job.created_at);
+export const headerTime = (state) => state.job.started ?? state.job.created_at;
export const hasForwardDeploymentFailure = (state) =>
state?.job?.failure_reason === 'forward_deployment_failure';
@@ -28,11 +28,9 @@ export const hasEnvironment = (state) => !isEmpty(state.job.deployment_status);
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 emptyStateIllustration = (state) => state?.job?.status?.illustration || {};
-export const emptyStateAction = (state) =>
- (state.job && state.job.status && state.job.status.action) || null;
+export const emptyStateAction = (state) => state?.job?.status?.action || null;
/**
* Shared runners limit is only rendered when
@@ -48,4 +46,4 @@ export const shouldRenderSharedRunnerLimitWarning = (state) =>
export const isScrollingDown = (state) => isScrolledToBottom() && !state.isTraceComplete;
export const hasRunnersForProject = (state) =>
- state.job.runners.available && !state.job.runners.online;
+ state?.job?.runners?.available && !state?.job?.runners?.online;
diff --git a/app/assets/javascripts/jobs/store/index.js b/app/assets/javascripts/jobs/store/index.js
index bba01426af7..467c692b438 100644
--- a/app/assets/javascripts/jobs/store/index.js
+++ b/app/assets/javascripts/jobs/store/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import state from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
+import state from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 92dffb87e1a..2a020a66fd2 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -2,11 +2,10 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
-
+import { dispose } from '~/tooltips';
import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
-import { hide, dispose } from '~/tooltips';
export default class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
@@ -30,7 +29,6 @@ export default class LabelManager {
}
bindEvents() {
- this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick);
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
}
@@ -46,11 +44,6 @@ export default class LabelManager {
_this.toggleEmptyState($label, $btn, action);
}
- onButtonActionClick(e) {
- e.stopPropagation();
- hide(e.currentTarget);
- }
-
toggleEmptyState() {
this.emptyState.classList.toggle(
'hidden',
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 337d063b02a..56e69ab9418 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,15 +4,15 @@
import $ from 'jquery';
import { difference, isEqual, escape, sortBy, template, union } from 'lodash';
-import { sprintf, __ } from './locale';
-import axios from './lib/utils/axios_utils';
-import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import boardsStore from './boards/stores/boards_store';
+import ModalStore from './boards/stores/modal_store';
import CreateLabelDropdown from './create_label';
import { deprecatedCreateFlash as flash } from './flash';
-import ModalStore from './boards/stores/modal_store';
-import boardsStore from './boards/stores/boards_store';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
+import axios from './lib/utils/axios_utils';
+import { sprintf, __ } from './locale';
export default class LabelsSelect {
constructor(els, options = {}) {
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 5c4bb5ea01f..bda5550a9f4 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -1,11 +1,11 @@
-import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
-import { createUploadLink } from 'apollo-upload-client';
+import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
+import { createUploadLink } from 'apollo-upload-client';
+import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
-import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
export const fetchPolicies = {
CACHE_FIRST: 'cache-first',
@@ -35,6 +35,16 @@ export default (resolvers = {}, config = {}) => {
batchMax: config.batchMax || 10,
};
+ const requestCounterLink = new ApolloLink((operation, forward) => {
+ window.pendingApolloRequests = window.pendingApolloRequests || 0;
+ window.pendingApolloRequests += 1;
+
+ return forward(operation).map((response) => {
+ window.pendingApolloRequests -= 1;
+ return response;
+ });
+ });
+
const uploadsLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions),
@@ -63,7 +73,12 @@ export default (resolvers = {}, config = {}) => {
return new ApolloClient({
typeDefs: config.typeDefs,
- link: ApolloLink.from([performanceBarLink, new StartupJSLink(), uploadsLink]),
+ link: ApolloLink.from([
+ requestCounterLink,
+ performanceBarLink,
+ new StartupJSLink(),
+ uploadsLink,
+ ]),
cache: new InMemoryCache({
...config.cacheConfig,
freezeResults: config.assumeImmutableResults,
diff --git a/app/assets/javascripts/lib/utils/array_utility.js b/app/assets/javascripts/lib/utils/array_utility.js
new file mode 100644
index 00000000000..197e7790ed7
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/array_utility.js
@@ -0,0 +1,20 @@
+/**
+ * Return a shallow copy of an array with two items swapped.
+ *
+ * @param {Array} array - The source array
+ * @param {Number} leftIndex - Index of the first item
+ * @param {Number} rightIndex - Index of the second item
+ * @returns {Array} new array with the left and right items swapped
+ */
+export const swapArrayItems = (array, leftIndex = 0, rightIndex = 0) => {
+ const copy = array.slice();
+
+ if (leftIndex >= array.length || leftIndex < 0 || rightIndex >= array.length || rightIndex < 0) {
+ return copy;
+ }
+
+ const temp = copy[leftIndex];
+ copy[leftIndex] = copy[rightIndex];
+ copy[rightIndex] = temp;
+ return copy;
+};
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index cb479e243b2..204c84b879e 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -1,7 +1,7 @@
import axios from 'axios';
+import setupAxiosStartupCalls from './axios_startup_calls';
import csrf from './csrf';
import suppressAjaxErrorsDuringNavigation from './suppress_ajax_errors_during_navigation';
-import setupAxiosStartupCalls from './axios_startup_calls';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
// Used by Rails to check if it is a valid XHR request
diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js
index a1f56b15631..ff176f11867 100644
--- a/app/assets/javascripts/lib/utils/color_utils.js
+++ b/app/assets/javascripts/lib/utils/color_utils.js
@@ -23,3 +23,23 @@ export const textColorForBackground = (backgroundColor) => {
}
return '#FFFFFF';
};
+
+/**
+ * Check whether a color matches the expected hex format
+ *
+ * This matches any hex (0-9 and A-F) value which is either 3 or 6 characters in length
+ *
+ * An empty string will return `null` which means that this is neither valid nor invalid.
+ * This is useful for forms resetting the validation state
+ *
+ * @param color string = ''
+ *
+ * @returns {null|boolean}
+ */
+export const validateHexColor = (color = '') => {
+ if (!color) {
+ return null;
+ }
+
+ return /^#([0-9A-F]{3}){1,2}$/i.test(color);
+};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 128ef5b335e..73eadfe3cbe 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -4,11 +4,11 @@
import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
-import { isFunction, defer } from 'lodash';
import Cookies from 'js-cookie';
-import { getLocationHash } from './url_utility';
+import { isFunction, defer } from 'lodash';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
+import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => {
const page = $('body').attr('data-page') || '';
@@ -45,6 +45,7 @@ export const checkPageAndAction = (page, action) => {
export const isInIncidentPage = () => checkPageAndAction('incidents', 'show');
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
+export const isInDesignPage = () => checkPageAndAction('issues', 'designs');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
@@ -122,7 +123,7 @@ export const handleLocationHash = () => {
}
if (isInIssuePage()) {
- adjustment -= fixedIssuableTitle.offsetHeight;
+ adjustment -= fixedIssuableTitle?.offsetHeight;
}
if (isInMRPage()) {
@@ -801,3 +802,12 @@ export const removeCookie = (name) => Cookies.remove(name);
* @returns {Boolean} on/off
*/
export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag];
+
+/**
+ * This method takes in array with snake_case strings
+ * and returns a new array with camelCase strings
+ *
+ * @param {Array[String]} array - Array to be converted
+ * @returns {Array[String]} Converted array
+ */
+export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i));
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 993d51370ec..b19a4a01a5f 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -10,3 +10,9 @@ export const DATETIME_RANGE_TYPES = {
open: 'open',
invalid: 'invalid',
};
+
+export const BV_SHOW_MODAL = 'bv::show::modal';
+export const BV_HIDE_MODAL = 'bv::hide::modal';
+export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip';
+export const BV_DROPDOWN_SHOW = 'bv::dropdown::show';
+export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide';
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 15f7c0c874e..38b3b26dc44 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,10 +1,12 @@
+import dateFormat from 'dateformat';
import $ from 'jquery';
import { isString, mapValues, isNumber, reduce } from 'lodash';
import * as timeago from 'timeago.js';
-import dateFormat from 'dateformat';
import { languageCode, s__, __, n__ } from '../../locale';
-const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
+const MILLISECONDS_IN_HOUR = 60 * 60 * 1000;
+const MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR;
+const DAYS_IN_WEEK = 7;
window.timeago = timeago;
@@ -674,50 +676,127 @@ export const secondsToHours = (offset) => {
};
/**
- * Returns the date n days after the date provided
+ * Returns the date `n` days after the date provided
*
* @param {Date} date the initial date
* @param {Number} numberOfDays number of days after
- * @return {Date} the date following the date provided
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
+ * @return {Date} A `Date` object `n` days after the provided `Date`
*/
-export const nDaysAfter = (date, numberOfDays) =>
- new Date(newDate(date)).setDate(date.getDate() + numberOfDays);
+export const nDaysAfter = (date, numberOfDays, { utc = false } = {}) => {
+ const clone = newDate(date);
+
+ const cloneValue = utc
+ ? clone.setUTCDate(date.getUTCDate() + numberOfDays)
+ : clone.setDate(date.getDate() + numberOfDays);
+
+ return new Date(cloneValue);
+};
/**
- * Returns the date n days before the date provided
+ * Returns the date `n` days before the date provided
*
* @param {Date} date the initial date
* @param {Number} numberOfDays number of days before
- * @return {Date} the date preceding the date provided
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ * @return {Date} A `Date` object `n` days before the provided `Date`
*/
-export const nDaysBefore = (date, numberOfDays) => nDaysAfter(date, -numberOfDays);
+export const nDaysBefore = (date, numberOfDays, options) =>
+ nDaysAfter(date, -numberOfDays, options);
/**
- * Returns the date n months after the date provided
+ * Returns the date `n` weeks after the date provided
+ *
+ * @param {Date} date the initial date
+ * @param {Number} numberOfWeeks number of weeks after
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
+ * @return {Date} A `Date` object `n` weeks after the provided `Date`
+ */
+export const nWeeksAfter = (date, numberOfWeeks, options) =>
+ nDaysAfter(date, DAYS_IN_WEEK * numberOfWeeks, options);
+
+/**
+ * Returns the date `n` weeks before the date provided
+ *
+ * @param {Date} date the initial date
+ * @param {Number} numberOfWeeks number of weeks before
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
+ * @return {Date} A `Date` object `n` weeks before the provided `Date`
+ */
+export const nWeeksBefore = (date, numberOfWeeks, options) =>
+ nWeeksAfter(date, -numberOfWeeks, options);
+
+/**
+ * Returns the date `n` months after the date provided
*
* @param {Date} date the initial date
* @param {Number} numberOfMonths number of months after
- * @return {Date} the date following the date provided
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
+ * @return {Date} A `Date` object `n` months after the provided `Date`
*/
-export const nMonthsAfter = (date, numberOfMonths) =>
- new Date(newDate(date)).setMonth(date.getMonth() + numberOfMonths);
+export const nMonthsAfter = (date, numberOfMonths, { utc = false } = {}) => {
+ const clone = newDate(date);
+
+ const cloneValue = utc
+ ? clone.setUTCMonth(date.getUTCMonth() + numberOfMonths)
+ : clone.setMonth(date.getMonth() + numberOfMonths);
+
+ return new Date(cloneValue);
+};
/**
- * Returns the date n months before the date provided
+ * Returns the date `n` months before the date provided
*
* @param {Date} date the initial date
* @param {Number} numberOfMonths number of months before
- * @return {Date} the date preceding the date provided
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
+ * @return {Date} A `Date` object `n` months before the provided `Date`
*/
-export const nMonthsBefore = (date, numberOfMonths) => nMonthsAfter(date, -numberOfMonths);
+export const nMonthsBefore = (date, numberOfMonths, options) =>
+ nMonthsAfter(date, -numberOfMonths, options);
/**
* Returns the date after the date provided
*
* @param {Date} date the initial date
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC dates.
+ * This will cause Daylight Saving Time to be ignored. Defaults to `false`
+ * if not provided, which causes the calculation to be performed in the
+ * user's timezone.
+ *
* @return {Date} the date following the date provided
*/
-export const dayAfter = (date) => new Date(newDate(date).setDate(date.getDate() + 1));
+export const dayAfter = (date, options) => nDaysAfter(date, 1, options);
/**
* Mimics the behaviour of the rails distance_of_time_in_words function
@@ -859,17 +938,17 @@ export const format24HourTimeStringFromInt = (time) => {
*
* @param {Object} givenPeriodLeft - the first period to compare.
* @param {Object} givenPeriodRight - the second period to compare.
- * @returns {Object} { overlap: number of days the overlap is present, overlapStartDate: the start date of the overlap in time format, overlapEndDate: the end date of the overlap in time format }
+ * @returns {Object} { daysOverlap: number of days the overlap is present, hoursOverlap: number of hours the overlap is present, overlapStartDate: the start date of the overlap in time format, overlapEndDate: the end date of the overlap in time format }
* @throws {Error} Uncaught Error: Invalid period
*
* @example
- * getOverlappingDaysInPeriods(
+ * getOverlapDateInPeriods(
* { start: new Date(2021, 0, 11), end: new Date(2021, 0, 13) },
* { start: new Date(2021, 0, 11), end: new Date(2021, 0, 14) }
- * ) => { daysOverlap: 2, overlapStartDate: 1610323200000, overlapEndDate: 1610496000000 }
+ * ) => { daysOverlap: 2, hoursOverlap: 48, overlapStartDate: 1610323200000, overlapEndDate: 1610496000000 }
*
*/
-export const getOverlappingDaysInPeriods = (givenPeriodLeft = {}, givenPeriodRight = {}) => {
+export const getOverlapDateInPeriods = (givenPeriodLeft = {}, givenPeriodRight = {}) => {
const leftStartTime = new Date(givenPeriodLeft.start).getTime();
const leftEndTime = new Date(givenPeriodLeft.end).getTime();
const rightStartTime = new Date(givenPeriodRight.start).getTime();
@@ -890,8 +969,44 @@ export const getOverlappingDaysInPeriods = (givenPeriodLeft = {}, givenPeriodRig
const differenceInMs = overlapEndDate - overlapStartDate;
return {
+ hoursOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_HOUR),
daysOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_DAY),
overlapStartDate,
overlapEndDate,
};
};
+
+/**
+ * A utility function that checks that the date is today
+ *
+ * @param {Date} date
+ *
+ * @return {Boolean} true if provided date is today
+ */
+export const isToday = (date) => {
+ const today = new Date();
+ return (
+ date.getDate() === today.getDate() &&
+ date.getMonth() === today.getMonth() &&
+ date.getFullYear() === today.getFullYear()
+ );
+};
+
+/**
+ * Returns the start of the provided day
+ *
+ * @param {Object} [options={}] Additional options for this calculation
+ * @param {boolean} [options.utc=false] Performs the calculation using UTC time.
+ * If `true`, the time returned will be midnight UTC. If `false` (the default)
+ * the time returned will be midnight in the user's local time.
+ *
+ * @returns {Date} A new `Date` object that represents the start of the day
+ * of the provided date
+ */
+export const getStartOfDay = (date, { utc = false } = {}) => {
+ const clone = newDate(date);
+
+ const cloneValue = utc ? clone.setUTCHours(0, 0, 0, 0) : clone.setHours(0, 0, 0, 0);
+
+ return new Date(cloneValue);
+};
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index d49382733c0..0f29f538b07 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -1,5 +1,5 @@
-import { BYTES_IN_KIB } from './constants';
import { sprintf, __ } from '~/locale';
+import { BYTES_IN_KIB } from './constants';
/**
* Function that allows a number with an X amount of decimals
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 6ec1bd206e6..71782c9a4ce 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -1,5 +1,5 @@
-import httpStatusCodes, { successCodes } from './http_status';
import { normalizeHeaders } from './common_utils';
+import httpStatusCodes, { successCodes } from './http_status';
/**
* Polling utility for handling realtime updates.
diff --git a/app/assets/javascripts/lib/utils/poll_until_complete.js b/app/assets/javascripts/lib/utils/poll_until_complete.js
index d3b551ca755..3545db3a227 100644
--- a/app/assets/javascripts/lib/utils/poll_until_complete.js
+++ b/app/assets/javascripts/lib/utils/poll_until_complete.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
-import Poll from './poll';
import httpStatusCodes from './http_status';
+import Poll from './poll';
/**
* Polls an endpoint until it returns either a 200 OK or a error status.
diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js
index 6bb7f09b886..a6d53358cb8 100644
--- a/app/assets/javascripts/lib/utils/sticky.js
+++ b/app/assets/javascripts/lib/utils/sticky.js
@@ -1,5 +1,3 @@
-import StickyFill from 'stickyfilljs';
-
export const createPlaceholder = () => {
const placeholder = document.createElement('div');
placeholder.classList.add('sticky-placeholder');
@@ -60,13 +58,3 @@ export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => {
},
);
};
-
-/**
- * Polyfill the `position: sticky` behavior.
- *
- * - If the current environment supports `position: sticky`, do nothing.
- * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement.
- */
-export const polyfillSticky = (el) => {
- StickyFill.add(el);
-};
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 2c993c8b128..5e66aa05218 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, no-param-reassign, operator-assignment, consistent-return */
import $ from 'jquery';
-import { insertText } from '~/lib/utils/common_utils';
import Shortcuts from '~/behaviors/shortcuts/shortcuts';
+import { insertText } from '~/lib/utils/common_utils';
const LINK_TAG_PATTERN = '[{text}](url)';
diff --git a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
index 9d47a1b7132..15f9512fe92 100644
--- a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
+++ b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
@@ -20,8 +20,9 @@ function formatNumber(
return '';
}
+ const locale = document.documentElement.lang || undefined;
const num = value * valueFactor;
- const formatted = num.toLocaleString(undefined, {
+ const formatted = num.toLocaleString(locale, {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits,
style,
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 44d3e78b334..cc2cf787a8f 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -16,6 +16,50 @@ function decodeUrlParameter(val) {
return decodeURIComponent(val.replace(/\+/g, '%20'));
}
+/**
+ * Safely encodes a string to be used as a path
+ *
+ * Note: This function DOES encode typical URL parts like ?, =, &, #, and +
+ * If you need to use search parameters or URL fragments, they should be
+ * added AFTER calling this function, not before.
+ *
+ * Usecase: An image filename is stored verbatim, and you need to load it in
+ * the browser.
+ *
+ * Example: /some_path/file #1.jpg ==> /some_path/file%20%231.jpg
+ * Example: /some-path/file! Final!.jpg ==> /some-path/file%21%20Final%21.jpg
+ *
+ * Essentially, if a character *could* present a problem in a URL, it's escaped
+ * to the hexadecimal representation instead. This means it's a bit more
+ * aggressive than encodeURIComponent: that built-in function doesn't
+ * encode some characters that *could* be problematic, so this function
+ * adds them (#, !, ~, *, ', (, and )).
+ * Additionally, encodeURIComponent *does* encode `/`, but we want safer
+ * URLs, not non-functional URLs, so this function DEcodes slashes ('%2F').
+ *
+ * @param {String} potentiallyUnsafePath
+ * @returns {String}
+ */
+export function encodeSaferUrl(potentiallyUnsafePath) {
+ const unencode = ['%2F'];
+ const encode = ['#', '!', '~', '\\*', "'", '\\(', '\\)'];
+ let saferPath = encodeURIComponent(potentiallyUnsafePath);
+
+ unencode.forEach((code) => {
+ saferPath = saferPath.replace(new RegExp(code, 'g'), decodeURIComponent(code));
+ });
+ encode.forEach((code) => {
+ const encodedValue = code
+ .codePointAt(code.length - 1)
+ .toString(16)
+ .toUpperCase();
+
+ saferPath = saferPath.replace(new RegExp(code, 'g'), `%${encodedValue}`);
+ });
+
+ return saferPath;
+}
+
export function cleanLeadingSeparator(path) {
return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
}
@@ -310,6 +354,20 @@ export function isAbsoluteOrRootRelative(url) {
}
/**
+ * Returns true if url is an external URL
+ *
+ * @param {String} url
+ * @returns {Boolean}
+ */
+export function isExternal(url) {
+ if (isRootRelative(url)) {
+ return false;
+ }
+
+ return !url.includes(gon.gitlab_url);
+}
+
+/**
* Converts a relative path to an absolute or a root relative path depending
* on what is passed as a basePath.
*
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index 622c40e0f35..07a4d2deb0b 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -1,6 +1,9 @@
import { joinPaths } from '~/lib/utils/url_utility';
-// tell webpack to load assets from origin so that web workers don't break
+/**
+ * Tell webpack to load assets from origin so that web workers don't break
+ * See https://gitlab.com/gitlab-org/gitlab/-/issues/321656 for a fix
+ */
export function resetServiceWorkersPublicPath() {
// __webpack_public_path__ is a global variable that can be used to adjust
// the webpack publicPath setting at runtime.
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index a114b3c7d4d..5092c6905c4 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -1,6 +1,4 @@
<script>
-import { throttle } from 'lodash';
-import { mapActions, mapState, mapGetters } from 'vuex';
import {
GlSprintf,
GlAlert,
@@ -10,14 +8,15 @@ import {
GlDropdownDivider,
GlInfiniteScroll,
} from '@gitlab/ui';
+import { throttle } from 'lodash';
+import { mapActions, mapState, mapGetters } from 'vuex';
-import LogSimpleFilters from './log_simple_filters.vue';
-import LogAdvancedFilters from './log_advanced_filters.vue';
-import LogControlButtons from './log_control_buttons.vue';
-
-import { defaultTimeRange } from '~/vue_shared/constants';
import { timeRangeFromUrl } from '~/monitoring/utils';
+import { defaultTimeRange } from '~/vue_shared/constants';
import { formatDate } from '../utils';
+import LogAdvancedFilters from './log_advanced_filters.vue';
+import LogControlButtons from './log_control_buttons.vue';
+import LogSimpleFilters from './log_simple_filters.vue';
export default {
components: {
@@ -176,7 +175,7 @@ export default {
id="environments-dropdown"
:text="environments.current || managedApps.current"
:disabled="environments.isLoading"
- class="gl-mr-3 gl-mb-3 gl-display-flex gl-display-md-block js-environments-dropdown"
+ class="gl-mr-3 gl-mb-3 gl-display-flex gl-md-display-block js-environments-dropdown"
>
<gl-dropdown-section-header>
{{ s__('Environments|Environments') }}
diff --git a/app/assets/javascripts/logs/components/log_advanced_filters.vue b/app/assets/javascripts/logs/components/log_advanced_filters.vue
index 37fc4dc3735..9159ca5b9dc 100644
--- a/app/assets/javascripts/logs/components/log_advanced_filters.vue
+++ b/app/assets/javascripts/logs/components/log_advanced_filters.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlFilteredSearch } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { __, s__ } from '~/locale';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { timeRanges } from '~/vue_shared/constants';
diff --git a/app/assets/javascripts/logs/components/log_simple_filters.vue b/app/assets/javascripts/logs/components/log_simple_filters.vue
index ba30d4628c9..55bdd5f0088 100644
--- a/app/assets/javascripts/logs/components/log_simple_filters.vue
+++ b/app/assets/javascripts/logs/components/log_simple_filters.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
export default {
@@ -42,7 +42,7 @@ export default {
ref="podsDropdown"
:text="podDropdownText"
:disabled="disabled"
- class="gl-mr-3 gl-mb-3 gl-display-flex gl-display-md-block qa-pods-dropdown"
+ class="gl-mr-3 gl-mb-3 gl-display-flex gl-md-display-block qa-pods-dropdown"
>
<gl-dropdown-section-header>
{{ s__('Environments|Select pod') }}
diff --git a/app/assets/javascripts/logs/stores/actions.js b/app/assets/javascripts/logs/stores/actions.js
index a26e6f694c9..e813f91d2fa 100644
--- a/app/assets/javascripts/logs/stores/actions.js
+++ b/app/assets/javascripts/logs/stores/actions.js
@@ -1,7 +1,7 @@
-import { backOff } from '~/lib/utils/common_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
+import { backOff } from '~/lib/utils/common_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import httpStatusCodes from '~/lib/utils/http_status';
import { TOKEN_TYPE_POD_NAME, tracking, logExplorerOptions } from '../constants';
import trackLogs from '../logs_tracking_helper';
diff --git a/app/assets/javascripts/logs/stores/mutations.js b/app/assets/javascripts/logs/stores/mutations.js
index 147f562057f..21031838adf 100644
--- a/app/assets/javascripts/logs/stores/mutations.js
+++ b/app/assets/javascripts/logs/stores/mutations.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import * as types from './mutation_types';
const mapLine = ({ timestamp, pod, message }) => ({
timestamp,
diff --git a/app/assets/javascripts/logs/stores/state.js b/app/assets/javascripts/logs/stores/state.js
index fbe6589dd84..e2028621ac8 100644
--- a/app/assets/javascripts/logs/stores/state.js
+++ b/app/assets/javascripts/logs/stores/state.js
@@ -1,5 +1,5 @@
-import { timeRanges, defaultTimeRange } from '~/vue_shared/constants';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import { timeRanges, defaultTimeRange } from '~/vue_shared/constants';
export default () => ({
/**
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index ef0fef6085b..32ecc0036bd 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,4 +1,5 @@
/* global $ */
+/* eslint-disable import/order */
import jQuery from 'jquery';
import Cookies from 'js-cookie';
@@ -12,6 +13,12 @@ import './behaviors';
import applyGitLabUIConfig from '@gitlab/ui/dist/config';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { initRails } from '~/lib/utils/rails_ujs';
+import * as popovers from '~/popovers';
+import * as tooltips from '~/tooltips';
+import initAlertHandler from './alert_handler';
+import { deprecatedCreateFlash as Flash, removeFlashClickListener } from './flash';
+import initTodoToggle from './header';
+import initLayoutNav from './layout_nav';
import {
handleLocationHash,
addSelectOnFocusBehaviour,
@@ -21,25 +28,18 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// everything else
-import { deprecatedCreateFlash as Flash, removeFlashClickListener } from './flash';
-import initTodoToggle from './header';
-import initLayoutNav from './layout_nav';
-import initAlertHandler from './alert_handler';
-import './feature_highlight/feature_highlight_options';
+import initFeatureHighlight from './feature_highlight';
import LazyLoader from './lazy_loader';
+import { __ } from './locale';
import initLogoAnimation from './logo';
import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
+import initPersistentUserCallouts from './persistent_user_callouts';
+import { initUserTracking, initDefaultTrackers } from './tracking';
import initUsagePingConsent from './usage_ping_consent';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
-import initPersistentUserCallouts from './persistent_user_callouts';
-import { initUserTracking, initDefaultTrackers } from './tracking';
-import { __ } from './locale';
-
-import * as tooltips from '~/tooltips';
-import * as popovers from '~/popovers';
import 'ee_else_ce/main_ee';
@@ -72,7 +72,7 @@ if (gon?.disable_animations) {
// inject test utilities if necessary
if (process.env.NODE_ENV !== 'production' && gon?.test_env) {
disableJQueryAnimations();
- import(/* webpackMode: "eager" */ './test_utils/'); // eslint-disable-line no-unused-expressions
+ import(/* webpackMode: "eager" */ './test_utils/');
}
document.addEventListener('beforeunload', () => {
@@ -115,6 +115,7 @@ function deferredInitialisation() {
initFrequentItemDropdowns();
initPersistentUserCallouts();
initDefaultTrackers();
+ initFeatureHighlight();
const search = document.querySelector('#search');
if (search) {
diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js
index 04eaa0c77c3..540314f8f9b 100644
--- a/app/assets/javascripts/manual_ordering.js
+++ b/app/assets/javascripts/manual_ordering.js
@@ -1,11 +1,11 @@
import Sortable from 'sortablejs';
-import { s__ } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
getBoardSortableDefaultOptions,
sortableStart,
} from '~/boards/mixins/sortable_default_options';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
axios
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index 4cf4fdd12bf..a8edeaf5176 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import { Rails } from '~/lib/utils/rails_ujs';
-import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { disableButtonIfEmptyField } from '~/lib/utils/common_utils';
+import { Rails } from '~/lib/utils/rails_ujs';
import { __, sprintf } from '~/locale';
export default class Members {
diff --git a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
index fcb70dd45a6..35966be7363 100644
--- a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
@@ -1,8 +1,8 @@
<script>
+import { s__, sprintf } from '~/locale';
import ActionButtonGroup from './action_button_group.vue';
-import RemoveMemberButton from './remove_member_button.vue';
import ApproveAccessRequestButton from './approve_access_request_button.vue';
-import { s__, sprintf } from '~/locale';
+import RemoveMemberButton from './remove_member_button.vue';
export default {
name: 'AccessRequestActionButtons',
diff --git a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
index e8a53ff173d..83f266779f2 100644
--- a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState } from 'vuex';
import { GlButton, GlForm, GlTooltipDirective } from '@gitlab/ui';
+import { mapState } from 'vuex';
import csrf from '~/lib/utils/csrf';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
index 9a27348f146..0bcc85157f1 100644
--- a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
@@ -1,8 +1,8 @@
<script>
+import { s__, sprintf } from '~/locale';
import ActionButtonGroup from './action_button_group.vue';
import RemoveMemberButton from './remove_member_button.vue';
import ResendInviteButton from './resend_invite_button.vue';
-import { s__, sprintf } from '~/locale';
export default {
name: 'InviteActionButtons',
diff --git a/app/assets/javascripts/members/components/action_buttons/leave_button.vue b/app/assets/javascripts/members/components/action_buttons/leave_button.vue
index 443a962e0cf..f600a207b8d 100644
--- a/app/assets/javascripts/members/components/action_buttons/leave_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/leave_button.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
-import LeaveModal from '../modals/leave_modal.vue';
import { LEAVE_MODAL_ID } from '../../constants';
+import LeaveModal from '../modals/leave_modal.vue';
export default {
name: 'LeaveButton',
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
index 9d89cb40676..3b87c29c1bc 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions } from 'vuex';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { mapActions } from 'vuex';
import { s__ } from '~/locale';
export default {
@@ -31,6 +31,7 @@ export default {
:title="$options.i18n.buttonTitle"
:aria-label="$options.i18n.buttonTitle"
icon="remove"
+ data-qa-selector="delete_group_access_link"
@click="showRemoveGroupLinkModal(groupLink)"
/>
</template>
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index b0b7ff4ce9a..cb71be39ebc 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState } from 'vuex';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { mapState } from 'vuex';
export default {
name: 'RemoveMemberButton',
diff --git a/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue b/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue
index 1cc3fd17e98..261a6279920 100644
--- a/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState } from 'vuex';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { mapState } from 'vuex';
import csrf from '~/lib/utils/csrf';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
index 0e5df961782..f779d1755a5 100644
--- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
@@ -1,8 +1,8 @@
<script>
+import { s__, sprintf } from '~/locale';
import ActionButtonGroup from './action_button_group.vue';
-import RemoveMemberButton from './remove_member_button.vue';
import LeaveButton from './leave_button.vue';
-import { s__, sprintf } from '~/locale';
+import RemoveMemberButton from './remove_member_button.vue';
export default {
name: 'UserActionButtons',
diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/members/components/app.vue
index 34a2c67fa9f..27fceb7374e 100644
--- a/app/assets/javascripts/groups/members/components/app.vue
+++ b/app/assets/javascripts/members/components/app.vue
@@ -1,13 +1,13 @@
<script>
-import { mapState, mapMutations } from 'vuex';
import { GlAlert } from '@gitlab/ui';
-import MembersTable from '~/members/components/table/members_table.vue';
-import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
+import { mapState, mapMutations } from 'vuex';
import { scrollToElement } from '~/lib/utils/common_utils';
-import { HIDE_ERROR } from '~/members/store/mutation_types';
+import { HIDE_ERROR } from '../store/mutation_types';
+import FilterSortContainer from './filter_sort/filter_sort_container.vue';
+import MembersTable from './table/members_table.vue';
export default {
- name: 'GroupMembersApp',
+ name: 'MembersApp',
components: { MembersTable, FilterSortContainer, GlAlert },
computed: {
...mapState(['showError', 'errorMessage']),
diff --git a/app/assets/javascripts/members/components/avatars/user_avatar.vue b/app/assets/javascripts/members/components/avatars/user_avatar.vue
index e2264085e67..79dda3c1379 100644
--- a/app/assets/javascripts/members/components/avatars/user_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/user_avatar.vue
@@ -6,9 +6,9 @@ import {
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { generateBadges } from 'ee_else_ce/members/utils';
+import { glEmojiTag } from '~/emoji';
import { __ } from '~/locale';
import { AVATAR_SIZE } from '../../constants';
-import { glEmojiTag } from '~/emoji';
export default {
name: 'UserAvatar',
@@ -69,7 +69,10 @@ export default {
>
<template #meta>
<div v-if="statusEmoji" class="gl-p-1">
- <span v-safe-html:[$options.safeHtmlConfig]="glEmojiTag(statusEmoji)"></span>
+ <span
+ v-safe-html:[$options.safeHtmlConfig]="glEmojiTag(statusEmoji)"
+ class="user-status-emoji gl-mr-0"
+ ></span>
</div>
<div v-for="badge in badges" :key="badge.text" class="gl-p-1">
<gl-badge size="sm" :variant="badge.variant">
diff --git a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
index f869ecd392f..812a8626949 100644
--- a/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
+++ b/app/assets/javascripts/members/components/filter_sort/filter_sort_container.vue
@@ -19,7 +19,7 @@ export default {
</script>
<template>
- <div v-if="showContainer" class="gl-bg-gray-10 gl-p-3 gl-display-md-flex">
+ <div v-if="showContainer" class="gl-bg-gray-10 gl-p-3 gl-md-display-flex">
<members-filtered-search-bar v-if="filteredSearchBar.show" class="gl-p-3 gl-flex-grow-1" />
<sort-dropdown v-if="showSortDropdown" class="gl-p-3 gl-flex-shrink-0" />
</div>
diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
index cf7501d84fa..039ee9a0207 100644
--- a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
+++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
@@ -1,11 +1,11 @@
<script>
-import { mapState } from 'vuex';
import { GlFilteredSearchToken } from '@gitlab/ui';
-import { setUrlParams, queryToObject } from '~/lib/utils/url_utility';
+import { mapState } from 'vuex';
import { getParameterByName } from '~/lib/utils/common_utils';
+import { setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
-import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { SEARCH_TOKEN_TYPE, SORT_PARAM } from '~/members/constants';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
export default {
name: 'MembersFilteredSearchBar',
diff --git a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
index bcfe559768d..9fa8772faf4 100644
--- a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
+++ b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
@@ -1,9 +1,9 @@
<script>
-import { mapState } from 'vuex';
import { GlSorting, GlSortingItem } from '@gitlab/ui';
+import { mapState } from 'vuex';
import { visitUrl } from '~/lib/utils/url_utility';
-import { parseSortParam, buildSortHref } from '~/members/utils';
import { FIELDS } from '~/members/constants';
+import { parseSortParam, buildSortHref } from '~/members/utils';
export default {
name: 'SortDropdown',
diff --git a/app/assets/javascripts/members/components/modals/leave_modal.vue b/app/assets/javascripts/members/components/modals/leave_modal.vue
index d231c7eabfa..a0f978d85cc 100644
--- a/app/assets/javascripts/members/components/modals/leave_modal.vue
+++ b/app/assets/javascripts/members/components/modals/leave_modal.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState } from 'vuex';
import { GlModal, GlForm, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { mapState } from 'vuex';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
import { LEAVE_MODAL_ID } from '../../constants';
diff --git a/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
index 231d014a4ec..1ba6bf9aba6 100644
--- a/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlModal, GlSprintf, GlForm } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
import { REMOVE_GROUP_LINK_MODAL_ID } from '../../constants';
@@ -52,6 +52,7 @@ export default {
:action-primary="$options.actionPrimary"
:action-cancel="$options.actionCancel"
size="sm"
+ data-qa-selector="remove_group_link_modal_content"
@primary="handlePrimary"
@hide="hideRemoveGroupLinkModal"
>
diff --git a/app/assets/javascripts/members/components/table/member_action_buttons.vue b/app/assets/javascripts/members/components/table/member_action_buttons.vue
index c61ebec33bd..6f15f079d2d 100644
--- a/app/assets/javascripts/members/components/table/member_action_buttons.vue
+++ b/app/assets/javascripts/members/components/table/member_action_buttons.vue
@@ -1,9 +1,9 @@
<script>
-import UserActionButtons from '../action_buttons/user_action_buttons.vue';
+import { MEMBER_TYPES } from '../../constants';
+import AccessRequestActionButtons from '../action_buttons/access_request_action_buttons.vue';
import GroupActionButtons from '../action_buttons/group_action_buttons.vue';
import InviteActionButtons from '../action_buttons/invite_action_buttons.vue';
-import AccessRequestActionButtons from '../action_buttons/access_request_action_buttons.vue';
-import { MEMBER_TYPES } from '../../constants';
+import UserActionButtons from '../action_buttons/user_action_buttons.vue';
export default {
name: 'MemberActionButtons',
diff --git a/app/assets/javascripts/members/components/table/member_avatar.vue b/app/assets/javascripts/members/components/table/member_avatar.vue
index a1f98d4008a..92b757ffcba 100644
--- a/app/assets/javascripts/members/components/table/member_avatar.vue
+++ b/app/assets/javascripts/members/components/table/member_avatar.vue
@@ -1,8 +1,8 @@
<script>
import { kebabCase } from 'lodash';
-import UserAvatar from '../avatars/user_avatar.vue';
-import InviteAvatar from '../avatars/invite_avatar.vue';
import GroupAvatar from '../avatars/group_avatar.vue';
+import InviteAvatar from '../avatars/invite_avatar.vue';
+import UserAvatar from '../avatars/user_avatar.vue';
export default {
name: 'MemberAvatar',
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 16e0cd5ad4e..9a3edff19ff 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -1,18 +1,18 @@
<script>
-import { mapState } from 'vuex';
import { GlTable, GlBadge } from '@gitlab/ui';
+import { mapState } from 'vuex';
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
-import { FIELDS } from '../../constants';
import initUserPopovers from '~/user_popovers';
-import MemberAvatar from './member_avatar.vue';
-import MemberSource from './member_source.vue';
+import { FIELDS } from '../../constants';
+import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import CreatedAt from './created_at.vue';
+import ExpirationDatepicker from './expiration_datepicker.vue';
import ExpiresAt from './expires_at.vue';
import MemberActionButtons from './member_action_buttons.vue';
+import MemberAvatar from './member_avatar.vue';
+import MemberSource from './member_source.vue';
import RoleDropdown from './role_dropdown.vue';
-import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
-import ExpirationDatepicker from './expiration_datepicker.vue';
export default {
name: 'MembersTable',
@@ -32,7 +32,7 @@ export default {
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
},
computed: {
- ...mapState(['members', 'tableFields', 'tableAttrs', 'currentUserId', 'sourceId']),
+ ...mapState(['members', 'tableFields', 'tableAttrs', 'currentUserId']),
filteredFields() {
return FIELDS.filter(
(field) => this.tableFields.includes(field.key) && this.showField(field),
@@ -55,9 +55,9 @@ export default {
methods: {
hasActionButtons(member) {
return (
- canRemove(member, this.sourceId) ||
+ canRemove(member) ||
canResend(member) ||
- canUpdate(member, this.currentUserId, this.sourceId) ||
+ canUpdate(member, this.currentUserId) ||
canOverride(member)
);
},
@@ -80,7 +80,7 @@ export default {
return 'col-actions';
}
- return ['col-actions', 'gl-display-none!', 'gl-display-lg-table-cell!'];
+ return ['col-actions', 'gl-display-none!', 'gl-lg-display-table-cell!'];
},
tbodyTrAttr(member) {
return {
diff --git a/app/assets/javascripts/members/components/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue
index 20aa01b96bc..1f537740f94 100644
--- a/app/assets/javascripts/members/components/table/members_table_cell.vue
+++ b/app/assets/javascripts/members/components/table/members_table_cell.vue
@@ -19,7 +19,7 @@ export default {
},
},
computed: {
- ...mapState(['sourceId', 'currentUserId']),
+ ...mapState(['currentUserId']),
isGroup() {
return isGroup(this.member);
},
@@ -41,19 +41,19 @@ export default {
return MEMBER_TYPES.user;
},
isDirectMember() {
- return isDirectMember(this.member, this.sourceId);
+ return isDirectMember(this.member);
},
isCurrentUser() {
return isCurrentUser(this.member, this.currentUserId);
},
canRemove() {
- return canRemove(this.member, this.sourceId);
+ return canRemove(this.member);
},
canResend() {
return canResend(this.member);
},
canUpdate() {
- return canUpdate(this.member, this.currentUserId, this.sourceId);
+ return canUpdate(this.member, this.currentUserId);
},
},
render() {
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 77cb150bff6..f68a8814fee 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -98,3 +98,8 @@ export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id';
export const SEARCH_TOKEN_TYPE = 'filtered-search-term';
export const SORT_PARAM = 'sort';
+
+export const MEMBER_ACCESS_LEVEL_PROPERTY_NAME = 'access_level';
+
+export const GROUP_LINK_BASE_PROPERTY_NAME = 'group_link';
+export const GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME = 'group_access';
diff --git a/app/assets/javascripts/groups/members/index.js b/app/assets/javascripts/members/index.js
index 3ec874b8d36..fe174d9beb6 100644
--- a/app/assets/javascripts/groups/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -1,11 +1,11 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
-import { GlToast } from '@gitlab/ui';
-import { parseDataAttributes } from 'ee_else_ce/groups/members/utils';
+import { parseDataAttributes } from 'ee_else_ce/members/utils';
import App from './components/app.vue';
-import membersStore from '~/members/store';
+import membersStore from './store';
-export const initGroupMembersApp = (
+export const initMembersApp = (
el,
{
tableFields = [],
diff --git a/app/assets/javascripts/members/store/actions.js b/app/assets/javascripts/members/store/actions.js
index 4c31b3c9744..7b191dd85d0 100644
--- a/app/assets/javascripts/members/store/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
@@ -1,6 +1,6 @@
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility';
+import * as types from './mutation_types';
export const updateMemberRole = async ({ state, commit }, { memberId, accessLevel }) => {
try {
@@ -11,7 +11,7 @@ export const updateMemberRole = async ({ state, commit }, { memberId, accessLeve
commit(types.RECEIVE_MEMBER_ROLE_SUCCESS, { memberId, accessLevel });
} catch (error) {
- commit(types.RECEIVE_MEMBER_ROLE_ERROR);
+ commit(types.RECEIVE_MEMBER_ROLE_ERROR, { error });
throw error;
}
@@ -37,7 +37,7 @@ export const updateMemberExpiration = async ({ state, commit }, { memberId, expi
expiresAt: expiresAt ? formatDate(expiresAt, 'isoUtcDateTime') : null,
});
} catch (error) {
- commit(types.RECEIVE_MEMBER_EXPIRATION_ERROR);
+ commit(types.RECEIVE_MEMBER_EXPIRATION_ERROR, { error });
throw error;
}
diff --git a/app/assets/javascripts/members/store/index.js b/app/assets/javascripts/members/store/index.js
index 34c102999d2..45f4eefffc9 100644
--- a/app/assets/javascripts/members/store/index.js
+++ b/app/assets/javascripts/members/store/index.js
@@ -1,6 +1,6 @@
-import createState from 'ee_else_ce/members/store/state';
-import mutations from 'ee_else_ce/members/store/mutations';
import * as actions from 'ee_else_ce/members/store/actions';
+import mutations from 'ee_else_ce/members/store/mutations';
+import createState from 'ee_else_ce/members/store/state';
export default (initialState) => ({
state: createState(initialState),
diff --git a/app/assets/javascripts/members/store/mutations.js b/app/assets/javascripts/members/store/mutations.js
index 2415e744290..f4aac1571d6 100644
--- a/app/assets/javascripts/members/store/mutations.js
+++ b/app/assets/javascripts/members/store/mutations.js
@@ -13,10 +13,10 @@ export default {
Vue.set(member, 'accessLevel', accessLevel);
},
- [types.RECEIVE_MEMBER_ROLE_ERROR](state) {
- state.errorMessage = s__(
- "Members|An error occurred while updating the member's role, please try again.",
- );
+ [types.RECEIVE_MEMBER_ROLE_ERROR](state, { error }) {
+ state.errorMessage =
+ error.response?.data?.message ||
+ s__("Members|An error occurred while updating the member's role, please try again.");
state.showError = true;
},
[types.RECEIVE_MEMBER_EXPIRATION_SUCCESS](state, { memberId, expiresAt }) {
@@ -28,10 +28,12 @@ export default {
Vue.set(member, 'expiresAt', expiresAt);
},
- [types.RECEIVE_MEMBER_EXPIRATION_ERROR](state) {
- state.errorMessage = s__(
- "Members|An error occurred while updating the member's expiration date, please try again.",
- );
+ [types.RECEIVE_MEMBER_EXPIRATION_ERROR](state, { error }) {
+ state.errorMessage =
+ error.response?.data?.message ||
+ s__(
+ "Members|An error occurred while updating the member's expiration date, please try again.",
+ );
state.showError = true;
},
[types.HIDE_ERROR](state) {
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index 780b5a9df57..4de2dadb490 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -1,7 +1,17 @@
-import { __ } from '~/locale';
-import { getParameterByName } from '~/lib/utils/common_utils';
+import { isUndefined } from 'lodash';
+import {
+ getParameterByName,
+ convertObjectPropsToCamelCase,
+ parseBoolean,
+} from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
-import { FIELDS, DEFAULT_SORT } from './constants';
+import { __ } from '~/locale';
+import {
+ FIELDS,
+ DEFAULT_SORT,
+ GROUP_LINK_BASE_PROPERTY_NAME,
+ GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
+} from './constants';
export const generateBadges = (member, isCurrentUser) => [
{
@@ -25,26 +35,24 @@ export const isGroup = (member) => {
return Boolean(member.sharedWithGroup);
};
-export const isDirectMember = (member, sourceId) => {
- return isGroup(member) || member.source?.id === sourceId;
+export const isDirectMember = (member) => {
+ return isGroup(member) || member.isDirectMember;
};
export const isCurrentUser = (member, currentUserId) => {
return member.user?.id === currentUserId;
};
-export const canRemove = (member, sourceId) => {
- return isDirectMember(member, sourceId) && member.canRemove;
+export const canRemove = (member) => {
+ return isDirectMember(member) && member.canRemove;
};
export const canResend = (member) => {
return Boolean(member.invite?.canResend);
};
-export const canUpdate = (member, currentUserId, sourceId) => {
- return (
- !isCurrentUser(member, currentUserId) && isDirectMember(member, sourceId) && member.canUpdate
- );
+export const canUpdate = (member, currentUserId) => {
+ return !isCurrentUser(member, currentUserId) && isDirectMember(member) && member.canUpdate;
};
export const parseSortParam = (sortableFields) => {
@@ -95,3 +103,35 @@ export const buildSortHref = ({
// Defined in `ee/app/assets/javascripts/vue_shared/components/members/utils.js`
export const canOverride = () => false;
+
+export const parseDataAttributes = (el) => {
+ const { members, sourceId, memberPath, canManageMembers } = el.dataset;
+
+ return {
+ members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
+ sourceId: parseInt(sourceId, 10),
+ memberPath,
+ canManageMembers: parseBoolean(canManageMembers),
+ };
+};
+
+export const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({
+ accessLevel,
+ ...otherProperties
+}) => {
+ const accessLevelProperty = !isUndefined(accessLevel)
+ ? { [accessLevelPropertyName]: accessLevel }
+ : {};
+
+ return {
+ [basePropertyName]: {
+ ...accessLevelProperty,
+ ...otherProperties,
+ },
+ };
+};
+
+export const groupLinkRequestFormatter = baseRequestFormatter(
+ GROUP_LINK_BASE_PROPERTY_NAME,
+ GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
+);
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
index 338fbd9078a..6eaabbb3519 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
@@ -1,9 +1,12 @@
-/* eslint-disable no-param-reassign */
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it relies on
+// app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
+// for its template.
+/* eslint-disable no-param-reassign, @gitlab/no-runtime-template-compiler */
-import Vue from 'vue';
import { debounce } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
+import Vue from 'vue';
import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
((global) => {
@@ -90,9 +93,11 @@ import { __ } from '~/locale';
this.saved = true;
// This probably be better placed in the data provider
+ /* eslint-disable vue/no-mutating-props */
this.file.content = this.editor.getValue();
this.file.resolveEditChanged = this.file.content !== this.originalContent;
this.file.promptDiscardConfirmation = false;
+ /* eslint-enable vue/no-mutating-props */
},
resetEditorContent() {
if (this.fileLoaded) {
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
index bc926cb9155..47214e288ae 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js
@@ -1,4 +1,7 @@
-/* eslint-disable no-param-reassign */
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it relies on
+// app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
+// for its template.
+/* eslint-disable no-param-reassign, @gitlab/no-runtime-template-compiler */
import Vue from 'vue';
import actionsMixin from '../mixins/line_conflict_actions';
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
index bb306e74825..1d5946cd78a 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js
@@ -15,6 +15,9 @@ import utilsMixin from '../mixins/line_conflict_utils';
required: true,
},
},
+ // This is a true violation of @gitlab/no-runtime-template-compiler, as it
+ // has a template string.
+ // eslint-disable-next-line @gitlab/no-runtime-template-compiler
template: `
<table class="diff-wrap-lines code js-syntax-highlight">
<tr class="line_holder parallel" v-for="section in file.parallelLines">
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index 693f0b619a8..fb3444262ea 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -1,8 +1,8 @@
/* eslint-disable no-param-reassign, babel/camelcase, no-nested-ternary, no-continue */
import $ from 'jquery';
-import Vue from 'vue';
import Cookies from 'js-cookie';
+import Vue from 'vue';
import { s__ } from '~/locale';
((global) => {
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 229f6f3e339..e3972b8b574 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,15 +1,19 @@
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it
+// relies on app/views/projects/merge_requests/conflicts/show.html.haml for its
+// template.
+/* eslint-disable @gitlab/no-runtime-template-compiler */
import $ from 'jquery';
import Vue from 'vue';
+import { __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { deprecatedCreateFlash as createFlash } from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
+import syntaxHighlight from '../syntax_highlight';
import MergeConflictsService from './merge_conflict_service';
import './components/diff_file_editor';
import './components/inline_conflict_lines';
import './components/parallel_conflict_lines';
-import syntaxHighlight from '../syntax_highlight';
-import { __ } from '~/locale';
export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index bf9e0a309dd..1a0156f8c0e 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,14 +1,14 @@
/* eslint-disable func-names, no-underscore-dangle, consistent-return */
import $ from 'jquery';
-import axios from './lib/utils/axios_utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import TaskList from './task_list';
-import MergeRequestTabs from './merge_request_tabs';
+import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
import { getParameterValues, setUrlParams } from './lib/utils/url_utility';
+import MergeRequestTabs from './merge_request_tabs';
+import TaskList from './task_list';
function MergeRequest(opts) {
// Initialize MergeRequest behavior
diff --git a/app/assets/javascripts/merge_request/components/status_box.vue b/app/assets/javascripts/merge_request/components/status_box.vue
index fd99802caff..5d2660d65e6 100644
--- a/app/assets/javascripts/merge_request/components/status_box.vue
+++ b/app/assets/javascripts/merge_request/components/status_box.vue
@@ -5,12 +5,14 @@ import mrEventHub from '../eventhub';
const CLASSES = {
opened: 'status-box-open',
+ locked: 'status-box-open',
closed: 'status-box-mr-closed',
merged: 'status-box-mr-merged',
};
const STATUS = {
opened: [__('Open'), 'issue-open-m'],
+ locked: [__('Open'), 'issue-open-m'],
closed: [__('Closed'), 'close'],
merged: [__('Merged'), 'git-merge'],
};
@@ -59,10 +61,10 @@ export default {
<div :class="statusBoxClass" class="issuable-status-box status-box">
<gl-icon
:name="statusIconName"
- class="gl-display-block gl-display-sm-none!"
+ class="gl-display-block gl-sm-display-none!"
data-testid="status-icon"
/>
- <span class="gl-display-none gl-display-sm-block">
+ <span class="gl-display-none gl-sm-display-block">
{{ statusHumanName }}
</span>
</div>
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 81241cd2418..4dab796d8a4 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,13 +1,16 @@
/* eslint-disable no-new, class-methods-use-this */
-
-import $ from 'jquery';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import $ from 'jquery';
import Cookies from 'js-cookie';
+import Vue from 'vue';
+import CommitPipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import createEventHub from '~/helpers/event_hub_factory';
-import axios from './lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from './flash';
+import initAddContextCommitsTriggers from './add_context_commits_modal';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
+import Diff from './diff';
+import { deprecatedCreateFlash as flash } from './flash';
import initChangesDropdown from './init_changes_dropdown';
+import axios from './lib/utils/axios_utils';
import {
parseUrlPathname,
handleLocationHash,
@@ -15,15 +18,12 @@ import {
parseBoolean,
scrollToElement,
} from './lib/utils/common_utils';
+import { localTimeAgo } from './lib/utils/datetime_utility';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { getLocationHash } from './lib/utils/url_utility';
-import Diff from './diff';
-import { localTimeAgo } from './lib/utils/datetime_utility';
-import syntaxHighlight from './syntax_highlight';
-import Notes from './notes';
-import { polyfillSticky } from './lib/utils/sticky';
-import initAddContextCommitsTriggers from './add_context_commits_modal';
import { __ } from './locale';
+import Notes from './notes';
+import syntaxHighlight from './syntax_highlight';
// MergeRequestTabs
//
@@ -123,7 +123,6 @@ export default class MergeRequestTabs {
) {
this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click();
}
- this.initAffix();
}
bindEvents() {
@@ -352,26 +351,30 @@ export default class MergeRequestTabs {
mountPipelinesView() {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- const { CommitPipelinesTable, mrWidgetData } = gl;
-
- this.commitPipelinesTable = new CommitPipelinesTable({
- propsData: {
- endpoint: pipelineTableViewEl.dataset.endpoint,
- helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
- emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
- errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
- autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
- canCreatePipelineInTargetProject: Boolean(
- mrWidgetData?.can_create_pipeline_in_target_project,
- ),
- sourceProjectFullPath: mrWidgetData?.source_project_full_path || '',
- targetProjectFullPath: mrWidgetData?.target_project_full_path || '',
- projectId: pipelineTableViewEl.dataset.projectId,
- mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
- },
+ const { mrWidgetData } = gl;
+
+ this.commitPipelinesTable = new Vue({
provide: {
targetProjectFullPath: mrWidgetData?.target_project_full_path || '',
},
+ render(createElement) {
+ return createElement(CommitPipelinesTable, {
+ props: {
+ endpoint: pipelineTableViewEl.dataset.endpoint,
+ helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
+ emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
+ errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
+ autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
+ canCreatePipelineInTargetProject: Boolean(
+ mrWidgetData?.can_create_pipeline_in_target_project,
+ ),
+ sourceProjectFullPath: mrWidgetData?.source_project_full_path || '',
+ targetProjectFullPath: mrWidgetData?.target_project_full_path || '',
+ projectId: pipelineTableViewEl.dataset.projectId,
+ mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
+ },
+ });
+ },
}).$mount();
// $mount(el) replaces the el with the new rendered component. We need it in order to mount
@@ -509,21 +512,4 @@ export default class MergeRequestTabs {
}
}, 0);
}
-
- initAffix() {
- const $tabs = $('.js-tabs-affix');
-
- // Screen space on small screens is usually very sparse
- // So we dont affix the tabs on these
- if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;
-
- /**
- If the browser does not support position sticky, it returns the position as static.
- If the browser does support sticky, then we allow the browser to handle it, if not
- then we default back to Bootstraps affix
- */
- if ($tabs.css('position') !== 'static') return;
-
- polyfillSticky($tabs);
- }
}
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 717766578de..f249fef5ea4 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
-import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from './flash';
-import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
+import axios from './lib/utils/axios_utils';
import { __ } from './locale';
+import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
export default class Milestone {
constructor() {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 921925e15c5..4cbe0a53307 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -4,16 +4,16 @@
import $ from 'jquery';
import { template, escape } from 'lodash';
-import { __, sprintf } from '~/locale';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import Api from '~/api';
-import axios from './lib/utils/axios_utils';
-import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility';
-import ModalStore from './boards/stores/modal_store';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __, sprintf } from '~/locale';
import boardsStore, {
boardStoreIssueSet,
boardStoreIssueDelete,
} from './boards/stores/boards_store';
+import ModalStore from './boards/stores/modal_store';
+import axios from './lib/utils/axios_utils';
+import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
@@ -95,14 +95,19 @@ export default class MilestoneSelect {
name: m.title,
}))
.sort((mA, mB) => {
+ const dueDateA = mA.due_date ? parsePikadayDate(mA.due_date) : null;
+ const dueDateB = mB.due_date ? parsePikadayDate(mB.due_date) : null;
+
// Move all expired milestones to the bottom.
- if (mA.expired) {
- return 1;
- }
- if (mB.expired) {
- return -1;
- }
- return 0;
+ if (mA.expired) return 1;
+ if (mB.expired) return -1;
+
+ // Move milestones without due dates just above expired milestones.
+ if (!dueDateA) return 1;
+ if (!dueDateB) return -1;
+
+ // Sort by due date in ascending order.
+ return dueDateA - dueDateB;
}),
)
.then((data) => {
diff --git a/app/assets/javascripts/milestones/components/milestone_results_section.vue b/app/assets/javascripts/milestones/components/milestone_results_section.vue
index d53a59e58d4..b866977b974 100644
--- a/app/assets/javascripts/milestones/components/milestone_results_section.vue
+++ b/app/assets/javascripts/milestones/components/milestone_results_section.vue
@@ -77,12 +77,7 @@ export default {
</div>
</template>
<template v-else>
- <gl-dropdown-item
- v-for="{ title } in items"
- :key="title"
- role="milestone option"
- @click="$emit('selected', title)"
- >
+ <gl-dropdown-item v-for="{ title } in items" :key="title" @click="$emit('selected', title)">
<span class="gl-pl-6" :class="{ 'selected-item': isSelectedMilestone(title) }">
{{ title }}
</span>
diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js
index f7200f22471..a26c8f85958 100644
--- a/app/assets/javascripts/mirrors/mirror_repos.js
+++ b/app/assets/javascripts/mirrors/mirror_repos.js
@@ -1,10 +1,10 @@
import $ from 'jquery';
import { debounce } from 'lodash';
-import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import SSHMirror from './ssh_mirror';
+import { __ } from '~/locale';
import { hide } from '~/tooltips';
+import SSHMirror from './ssh_mirror';
export default class MirrorRepos {
constructor(container) {
diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js
index b692db10e2d..15ded478405 100644
--- a/app/assets/javascripts/mirrors/ssh_mirror.js
+++ b/app/assets/javascripts/mirrors/ssh_mirror.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import { escape } from 'lodash';
-import { __ } from '~/locale';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
import AUTH_METHOD from './constants';
export default class SSHMirror {
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue
index bf31b86561a..9724ef9950b 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget.vue
@@ -1,12 +1,13 @@
<script>
import { GlBadge, GlLoadingIcon, GlModalDirective, GlIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
import { values, get } from 'lodash';
-import { s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import AlertWidgetForm from './alert_widget_form.vue';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
+import { s__ } from '~/locale';
+import { OPERATORS } from '../constants';
import AlertsService from '../services/alerts_service';
import { alertsValidator, queriesValidator } from '../validators';
-import { OPERATORS } from '../constants';
+import AlertWidgetForm from './alert_widget_form.vue';
export default {
components: {
@@ -165,11 +166,11 @@ export default {
return get(alertQuery, 'result[0].values', []).map((value) => get(value, '[1]', null));
},
showModal() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
hideModal() {
this.errorMessage = null;
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
handleSetApiAction(apiAction) {
this.apiAction = apiAction;
diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
index b26941891e4..68fd3e256ec 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget_form.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
@@ -1,6 +1,4 @@
<script>
-import { isEmpty, findKey } from 'lodash';
-import Vue from 'vue';
import {
GlLink,
GlButton,
@@ -13,12 +11,14 @@ import {
GlTooltipDirective,
GlIcon,
} from '@gitlab/ui';
+import { isEmpty, findKey } from 'lodash';
+import Vue from 'vue';
import { __, s__ } from '~/locale';
-import Translate from '~/vue_shared/translate';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { alertsValidator, queriesValidator } from '../validators';
+import Translate from '~/vue_shared/translate';
import { OPERATORS } from '../constants';
+import { alertsValidator, queriesValidator } from '../validators';
Vue.use(Translate);
diff --git a/app/assets/javascripts/monitoring/components/charts/anomaly.vue b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
index cb533c38fa0..8569a67da34 100644
--- a/app/assets/javascripts/monitoring/components/charts/anomaly.vue
+++ b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
@@ -1,9 +1,9 @@
<script>
+import { GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import produce from 'immer';
import { flattenDeep, isNumber } from 'lodash';
-import { GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
-import { roundOffFloat } from '~/lib/utils/common_utils';
import { hexToRgb } from '~/lib/utils/color_utils';
+import { roundOffFloat } from '~/lib/utils/common_utils';
import { areaOpacityValues, symbolSizes, colorValues, panelTypes } from '../../constants';
import { graphDataValidatorForAnomalyValues } from '../../utils';
import MonitorTimeSeriesChart from './time_series.vue';
diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue
index ba947c2fa9c..37251af2049 100644
--- a/app/assets/javascripts/monitoring/components/charts/column.vue
+++ b/app/assets/javascripts/monitoring/components/charts/column.vue
@@ -1,12 +1,12 @@
<script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import { makeDataSeries } from '~/helpers/monitor_helper';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { chartHeight } from '../../constants';
-import { makeDataSeries } from '~/helpers/monitor_helper';
+import { timezones } from '../../format_date';
import { graphDataValidatorForValues } from '../../utils';
import { getTimeAxisOptions, getYAxisOptions, getChartGrid } from './options';
-import { timezones } from '../../format_date';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/charts/gauge.vue b/app/assets/javascripts/monitoring/components/charts/gauge.vue
index 63fa60bbdf0..461ff06be72 100644
--- a/app/assets/javascripts/monitoring/components/charts/gauge.vue
+++ b/app/assets/javascripts/monitoring/components/charts/gauge.vue
@@ -2,9 +2,9 @@
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlGaugeChart } from '@gitlab/ui/dist/charts';
import { isFinite, isArray, isInteger } from 'lodash';
+import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { graphDataValidatorForValues } from '../../utils';
import { getValidThresholds } from './options';
-import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
index 22214a76aba..ed888ef022c 100644
--- a/app/assets/javascripts/monitoring/components/charts/heatmap.vue
+++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
@@ -1,8 +1,8 @@
<script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlHeatmap } from '@gitlab/ui/dist/charts';
-import { graphDataValidatorForValues } from '../../utils';
import { formatDate, timezones, formats } from '../../format_date';
+import { graphDataValidatorForValues } from '../../utils';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/charts/options.js b/app/assets/javascripts/monitoring/components/charts/options.js
index 163a7be6973..643550a7144 100644
--- a/app/assets/javascripts/monitoring/components/charts/options.js
+++ b/app/assets/javascripts/monitoring/components/charts/options.js
@@ -1,8 +1,8 @@
import { isFinite, uniq, sortBy, includes } from 'lodash';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import { __, s__ } from '~/locale';
-import { formatDate, timezones, formats } from '../../format_date';
import { thresholdModeTypes } from '../../constants';
+import { formatDate, timezones, formats } from '../../format_date';
const yAxisBoundaryGap = [0.1, 0.1];
/**
diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
index a8ab41ebf26..6d6a7af600b 100644
--- a/app/assets/javascripts/monitoring/components/charts/single_stat.vue
+++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
@@ -1,7 +1,7 @@
<script>
import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import { __ } from '~/locale';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
+import { __ } from '~/locale';
import { graphDataValidatorForValues } from '../../utils';
const defaultPrecision = 2;
@@ -45,12 +45,18 @@ export default {
}
if (this.graphData?.maxValue) {
- formatter = getFormatter(SUPPORTED_FORMATS.percent);
- return formatter(this.queryResult / Number(this.graphData.maxValue), defaultPrecision);
+ formatter = getFormatter(SUPPORTED_FORMATS.number);
+ return formatter(
+ (this.queryResult / Number(this.graphData.maxValue)) * 100,
+ defaultPrecision,
+ );
}
formatter = getFormatter(SUPPORTED_FORMATS.number);
- return `${formatter(this.queryResult, defaultPrecision)}${this.queryInfo.unit ?? ''}`;
+ return `${formatter(this.queryResult, defaultPrecision)}`;
+ },
+ unit() {
+ return this.graphData?.maxValue ? '%' : this.queryInfo.unit;
},
graphTitle() {
return this.queryInfo.label;
@@ -60,6 +66,6 @@ export default {
</script>
<template>
<div>
- <gl-single-stat :value="statValue" :title="graphTitle" variant="success" />
+ <gl-single-stat :value="statValue" :title="graphTitle" :unit="unit" variant="success" />
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
index b5ae6bcfd13..a53f899f752 100644
--- a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
+++ b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
@@ -2,11 +2,11 @@
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlStackedColumnChart } from '@gitlab/ui/dist/charts';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
-import { chartHeight, legendLayoutTypes } from '../../constants';
import { s__ } from '~/locale';
+import { chartHeight, legendLayoutTypes } from '../../constants';
+import { formats, timezones } from '../../format_date';
import { graphDataValidatorForValues } from '../../utils';
import { getTimeAxisOptions, axisTypes } from './options';
-import { formats, timezones } from '../../format_date';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index e9f7b11c977..99008d047af 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -1,15 +1,15 @@
<script>
-import { isEmpty, omit, throttle } from 'lodash';
import { GlLink, GlTooltip, GlResizeObserverDirective, GlIcon } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
-import { s__ } from '~/locale';
+import { isEmpty, omit, throttle } from 'lodash';
+import { makeDataSeries } from '~/helpers/monitor_helper';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import { s__ } from '~/locale';
import { panelTypes, chartHeight, lineTypes, lineWidths, legendLayoutTypes } from '../../constants';
-import { getYAxisOptions, getTimeAxisOptions, getChartGrid, getTooltipFormatter } from './options';
-import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
-import { makeDataSeries } from '~/helpers/monitor_helper';
-import { graphDataValidatorForValues } from '../../utils';
import { formatDate, timezones } from '../../format_date';
+import { graphDataValidatorForValues } from '../../utils';
+import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
+import { getYAxisOptions, getTimeAxisOptions, getChartGrid, getTooltipFormatter } from './options';
export const timestampToISODate = (timestamp) => new Date(timestamp).toISOString();
diff --git a/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue
index 74799002b17..bfaf8b2bd28 100644
--- a/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue
+++ b/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlModal, GlSprintf } from '@gitlab/ui';
-import { s__ } from '~/locale';
import { isSafeURL } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
export default {
components: { GlButton, GlModal, GlSprintf },
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 16c2c87a4b7..bfb18206b62 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,31 +1,30 @@
<script>
-import { mapActions, mapState, mapGetters } from 'vuex';
-import VueDraggable from 'vuedraggable';
-import Mousetrap from 'mousetrap';
import { GlButton, GlModalDirective, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import DashboardHeader from './dashboard_header.vue';
-import DashboardPanel from './dashboard_panel.vue';
-import { s__ } from '~/locale';
+import Mousetrap from 'mousetrap';
+import VueDraggable from 'vuedraggable';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import invalidUrl from '~/lib/utils/invalid_url';
import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
-import invalidUrl from '~/lib/utils/invalid_url';
-
-import GraphGroup from './graph_group.vue';
-import EmptyState from './empty_state.vue';
-import GroupEmptyState from './group_empty_state.vue';
-import VariablesSection from './variables_section.vue';
-import LinksSection from './links_section.vue';
-
+import { s__ } from '~/locale';
+import { defaultTimeRange } from '~/vue_shared/constants';
import TrackEventDirective from '~/vue_shared/directives/track_event';
+import { metricStates, keyboardShortcutKeys } from '../constants';
import {
timeRangeFromUrl,
panelToUrl,
expandedPanelPayloadFromUrl,
convertVariablesForURL,
} from '../utils';
-import { metricStates, keyboardShortcutKeys } from '../constants';
-import { defaultTimeRange } from '~/vue_shared/constants';
+import DashboardHeader from './dashboard_header.vue';
+import DashboardPanel from './dashboard_panel.vue';
+
+import EmptyState from './empty_state.vue';
+import GraphGroup from './graph_group.vue';
+import GroupEmptyState from './group_empty_state.vue';
+import LinksSection from './links_section.vue';
+import VariablesSection from './variables_section.vue';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
index 9d1926dca54..94cfb562ce3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -1,5 +1,4 @@
<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
import {
GlButton,
GlDropdown,
@@ -10,15 +9,16 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
+import { mapState, mapGetters, mapActions } from 'vuex';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
-import { PANEL_NEW_PAGE } from '../router/constants';
-import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
-import CreateDashboardModal from './create_dashboard_modal.vue';
-import { s__ } from '~/locale';
import invalidUrl from '~/lib/utils/invalid_url';
import { redirectTo } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
+import { PANEL_NEW_PAGE } from '../router/constants';
import { getAddMetricTrackingOptions } from '../utils';
+import CreateDashboardModal from './create_dashboard_modal.vue';
+import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index 0f6a9ce3814..3c423bea368 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -1,6 +1,4 @@
<script>
-import { debounce } from 'lodash';
-import { mapActions, mapState, mapGetters } from 'vuex';
import {
GlButton,
GlDropdown,
@@ -12,17 +10,18 @@ import {
GlTooltipDirective,
GlIcon,
} from '@gitlab/ui';
-import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
+import { debounce } from 'lodash';
+import { mapActions, mapState, mapGetters } from 'vuex';
import invalidUrl from '~/lib/utils/invalid_url';
+import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import DashboardsDropdown from './dashboards_dropdown.vue';
-import RefreshButton from './refresh_button.vue';
-import ActionsMenu from './dashboard_actions_menu.vue';
-
-import { timeRangeToUrl } from '../utils';
import { timeRanges } from '~/vue_shared/constants';
import { timezones } from '../format_date';
+import { timeRangeToUrl } from '../utils';
+import ActionsMenu from './dashboard_actions_menu.vue';
+import DashboardsDropdown from './dashboards_dropdown.vue';
+import RefreshButton from './refresh_button.vue';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 2b0c3d03b8d..55e73d17842 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -1,6 +1,4 @@
<script>
-import { mapState } from 'vuex';
-import { mapValues, pickBy } from 'lodash';
import {
GlResizeObserverDirective,
GlIcon,
@@ -15,26 +13,28 @@ import {
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
-import invalidUrl from '~/lib/utils/invalid_url';
+import { mapValues, pickBy } from 'lodash';
+import { mapState } from 'vuex';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import invalidUrl from '~/lib/utils/invalid_url';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import { __, n__ } from '~/locale';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
import { panelTypes } from '../constants';
-import MonitorEmptyChart from './charts/empty_chart.vue';
-import MonitorTimeSeriesChart from './charts/time_series.vue';
+import { graphDataToCsv } from '../csv_export';
+import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
+import AlertWidget from './alert_widget.vue';
import MonitorAnomalyChart from './charts/anomaly.vue';
-import MonitorSingleStatChart from './charts/single_stat.vue';
+import MonitorBarChart from './charts/bar.vue';
+import MonitorColumnChart from './charts/column.vue';
+import MonitorEmptyChart from './charts/empty_chart.vue';
import MonitorGaugeChart from './charts/gauge.vue';
import MonitorHeatmapChart from './charts/heatmap.vue';
-import MonitorColumnChart from './charts/column.vue';
-import MonitorBarChart from './charts/bar.vue';
+import MonitorSingleStatChart from './charts/single_stat.vue';
import MonitorStackedColumnChart from './charts/stacked_column.vue';
-
-import TrackEventDirective from '~/vue_shared/directives/track_event';
-import AlertWidget from './alert_widget.vue';
-import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
-import { graphDataToCsv } from '../csv_export';
+import MonitorTimeSeriesChart from './charts/time_series.vue';
const events = {
timeRangeZoom: 'timerangezoom',
@@ -318,7 +318,7 @@ export default {
return isSafeURL(url) ? url : '#';
},
showAlertModal() {
- this.$root.$emit('bv::show::modal', this.alertModalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.alertModalId);
},
showAlertModalFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
index bcfa1b04322..847339e814a 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapState } from 'vuex';
import {
GlCard,
GlForm,
@@ -10,6 +9,7 @@ import {
GlAlert,
GlTooltipDirective,
} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { timeRanges } from '~/vue_shared/constants';
import DashboardPanel from './dashboard_panel.vue';
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index 1a349aa154a..1238996154d 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -1,5 +1,4 @@
<script>
-import { mapState, mapGetters } from 'vuex';
import {
GlIcon,
GlDropdown,
@@ -9,6 +8,7 @@ import {
GlSearchBoxByType,
GlModalDirective,
} from '@gitlab/ui';
+import { mapState, mapGetters } from 'vuex';
const events = {
selectDashboard: 'selectDashboard',
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
index b87934a1db2..49d7e3a48a7 100644
--- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import DuplicateDashboardForm from './duplicate_dashboard_form.vue';
diff --git a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue
index c114ae1809f..8eef3d69a4f 100644
--- a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue
+++ b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue
@@ -1,7 +1,7 @@
<script>
-import { mapState, mapActions, mapGetters } from 'vuex';
-import sum from 'lodash/sum';
import { GlButton, GlCard, GlIcon } from '@gitlab/ui';
+import sum from 'lodash/sum';
+import { mapState, mapActions, mapGetters } from 'vuex';
import { n__ } from '~/locale';
import { monitoringDashboard } from '~/monitoring/stores';
import MetricEmbed from './metric_embed.vue';
diff --git a/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue b/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue
index 2fe49152c4f..25500747573 100644
--- a/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue
+++ b/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue
@@ -1,10 +1,10 @@
<script>
import { mapState, mapActions } from 'vuex';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { defaultTimeRange } from '~/vue_shared/constants';
-import { timeRangeFromUrl, removeTimeRangeParams } from '../../utils';
import { sidebarAnimationDuration } from '../../constants';
+import { timeRangeFromUrl, removeTimeRangeParams } from '../../utils';
let sidebarMutationObserver;
diff --git a/app/assets/javascripts/monitoring/components/links_section.vue b/app/assets/javascripts/monitoring/components/links_section.vue
index ca1e9c4d0d4..3f9f57d4ac1 100644
--- a/app/assets/javascripts/monitoring/components/links_section.vue
+++ b/app/assets/javascripts/monitoring/components/links_section.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlIcon, GlLink } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
export default {
components: {
@@ -15,12 +15,12 @@ export default {
<template>
<div
ref="linksSection"
- class="gl-display-sm-flex gl-flex-sm-wrap gl-mt-5 gl-p-3 gl-bg-gray-10 border gl-rounded-base links-section"
+ class="gl-sm-display-flex gl-flex-sm-wrap gl-mt-5 gl-p-3 gl-bg-gray-10 border gl-rounded-base links-section"
>
<div
v-for="(link, key) in links"
:key="key"
- class="gl-mb-1 gl-mr-5 gl-display-flex gl-display-sm-block gl-hover-text-blue-600-children gl-word-break-all"
+ class="gl-mb-1 gl-mr-5 gl-display-flex gl-sm-display-block gl-hover-text-blue-600-children gl-word-break-all"
>
<gl-link :href="link.url" class="gl-text-gray-900 gl-text-decoration-none!"
><gl-icon name="link" class="gl-text-gray-500 gl-vertical-align-text-bottom gl-mr-2" />{{
diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue
index e0d9f92440b..3daf5b38933 100644
--- a/app/assets/javascripts/monitoring/components/refresh_button.vue
+++ b/app/assets/javascripts/monitoring/components/refresh_button.vue
@@ -1,6 +1,4 @@
<script>
-import Visibility from 'visibilityjs';
-import { mapActions } from 'vuex';
import {
GlButtonGroup,
GlButton,
@@ -9,6 +7,8 @@ import {
GlDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
+import Visibility from 'visibilityjs';
+import { mapActions } from 'vuex';
import { n__, __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
index 7c4fb135ec8..493d37ce263 100644
--- a/app/assets/javascripts/monitoring/components/variables_section.vue
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -1,9 +1,9 @@
<script>
import { mapState, mapActions } from 'vuex';
+import { VARIABLE_TYPES } from '../constants';
+import { setCustomVariablesFromUrl } from '../utils';
import DropdownField from './variables/dropdown_field.vue';
import TextField from './variables/text_field.vue';
-import { setCustomVariablesFromUrl } from '../utils';
-import { VARIABLE_TYPES } from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 81ad3137b8b..060ed896d7c 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -151,7 +151,7 @@ export const linkTypes = {
* chart legend layout.
*
* Currently defined in
- * https://gitlab.com/gitlab-org/gitlab-ui/-/blob/master/src/utils/charts/constants.js
+ * https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/src/utils/charts/constants.js
*
*/
export const legendLayoutTypes = {
diff --git a/app/assets/javascripts/monitoring/monitoring_app.js b/app/assets/javascripts/monitoring/monitoring_app.js
index 307154c9a84..ee67e5dd827 100644
--- a/app/assets/javascripts/monitoring/monitoring_app.js
+++ b/app/assets/javascripts/monitoring/monitoring_app.js
@@ -1,7 +1,7 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import { createStore } from './stores';
+import Vue from 'vue';
import createRouter from './router';
+import { createStore } from './stores';
import { stateAndPropsFromDataset } from './utils';
Vue.use(GlToast);
@@ -26,7 +26,24 @@ export default (props = {}) => {
dashboardProps: { ...dataProps, ...props },
};
},
- template: `<router-view :dashboardProps="dashboardProps"/>`,
+ render(h) {
+ return h('RouterView', {
+ // This is attrs rather than props because:
+ // 1. RouterView only actually defines one prop: `name`.
+ // 2. The RouterView [throws away other props][1] given to it, in
+ // favour of those configured in the route config/params.
+ // 3. The Vue template compiler itself in general compiles anything
+ // like <some-component :foo="bar" /> into roughly
+ // h('some-component', { attrs: { foo: bar } }). Then later, Vue
+ // [extract props from attrs and merges them with props][2],
+ // matching them up according to the component's definition.
+ // [1]: https://github.com/vuejs/vue-router/blob/v3.4.9/src/components/view.js#L124
+ // [2]: https://github.com/vuejs/vue/blob/v2.6.12/src/core/vdom/helpers/extract-props.js#L12-L50
+ attrs: {
+ dashboardProps: this.dashboardProps,
+ },
+ });
+ },
});
}
};
diff --git a/app/assets/javascripts/monitoring/pages/panel_new_page.vue b/app/assets/javascripts/monitoring/pages/panel_new_page.vue
index 8ff6adb47ca..dbda6e80dac 100644
--- a/app/assets/javascripts/monitoring/pages/panel_new_page.vue
+++ b/app/assets/javascripts/monitoring/pages/panel_new_page.vue
@@ -1,9 +1,9 @@
<script>
-import { mapState } from 'vuex';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { mapState } from 'vuex';
import { s__ } from '~/locale';
-import { DASHBOARD_PAGE } from '../router/constants';
import DashboardPanelBuilder from '../components/dashboard_panel_builder.vue';
+import { DASHBOARD_PAGE } from '../router/constants';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/requests/index.js b/app/assets/javascripts/monitoring/requests/index.js
index 4a12ca06197..26fedb9c81c 100644
--- a/app/assets/javascripts/monitoring/requests/index.js
+++ b/app/assets/javascripts/monitoring/requests/index.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
-import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
+import statusCodes from '~/lib/utils/http_status';
import { PROMETHEUS_TIMEOUT } from '../constants';
const cancellableBackOffRequest = (makeRequestCallback) =>
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 44c200cdb54..8522ac6a57d 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -1,23 +1,23 @@
-import * as Sentry from '~/sentry/wrapper';
-import * as types from './mutation_types';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import * as Sentry from '~/sentry/wrapper';
+import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
+import { s__, sprintf } from '../../locale';
+import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
+import trackDashboardLoad from '../monitoring_tracking_helper';
+import getAnnotations from '../queries/getAnnotations.query.graphql';
+import getDashboardValidationWarnings from '../queries/getDashboardValidationWarnings.query.graphql';
+import getEnvironments from '../queries/getEnvironments.query.graphql';
+import { getDashboard, getPrometheusQueryData } from '../requests';
+
+import * as types from './mutation_types';
import {
gqClient,
parseEnvironmentsResponse,
parseAnnotationsResponse,
removeLeadingSlash,
} from './utils';
-import trackDashboardLoad from '../monitoring_tracking_helper';
-import getEnvironments from '../queries/getEnvironments.query.graphql';
-import getAnnotations from '../queries/getAnnotations.query.graphql';
-import getDashboardValidationWarnings from '../queries/getDashboardValidationWarnings.query.graphql';
-import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
-import { s__, sprintf } from '../../locale';
-import { getDashboard, getPrometheusQueryData } from '../requests';
-
-import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
const axiosCancelToken = axios.CancelToken;
let cancelTokenSource;
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 5c5a7d03b97..e0eaf76b5f6 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,10 +1,10 @@
-import Vue from 'vue';
import { pick } from 'lodash';
-import * as types from './mutation_types';
-import { mapToDashboardViewModel, mapPanelToViewModel, normalizeQueryResponseData } from './utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import Vue from 'vue';
import { BACKOFF_TIMEOUT } from '~/lib/utils/common_utils';
+import httpStatusCodes from '~/lib/utils/http_status';
import { dashboardEmptyStates, endpointKeys, initialStateKeys, metricStates } from '../constants';
+import * as types from './mutation_types';
+import { mapToDashboardViewModel, mapPanelToViewModel, normalizeQueryResponseData } from './utils';
import { optionsFromSeriesData } from './variable_mapping';
/**
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index ef8b1adb624..3883fa3380d 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -1,7 +1,7 @@
import invalidUrl from '~/lib/utils/invalid_url';
-import { timezones } from '../format_date';
-import { dashboardEmptyStates } from '../constants';
import { defaultTimeRange } from '~/vue_shared/constants';
+import { dashboardEmptyStates } from '../constants';
+import { timezones } from '../format_date';
export default () => ({
// API endpoints
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 36e5a135d59..20f7c5cdb60 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -1,12 +1,12 @@
-import { slugify } from '~/lib/utils/text_utility';
-import createGqClient, { fetchPolicies } from '~/lib/graphql';
-import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { mergeURLVariables, parseTemplatingVariables } from './variable_mapping';
+import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { DATETIME_RANGE_TYPES } from '~/lib/utils/constants';
import { timeRangeToParams, getRangeType } from '~/lib/utils/datetime_range';
+import { slugify } from '~/lib/utils/text_utility';
+import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { isSafeURL, mergeUrlParams } from '~/lib/utils/url_utility';
import { NOT_IN_DB_PREFIX, linkTypes, OUT_OF_THE_BOX_DASHBOARDS_PATH_PREFIX } from '../constants';
+import { mergeURLVariables, parseTemplatingVariables } from './variable_mapping';
export const gqClient = createGqClient(
{},
diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js
index c9e0e383582..4ca7a0b51d6 100644
--- a/app/assets/javascripts/monitoring/stores/variable_mapping.js
+++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js
@@ -1,6 +1,6 @@
import { isString } from 'lodash';
-import { templatingVariablesFromUrl } from '../utils';
import { VARIABLE_TYPES } from '../constants';
+import { templatingVariablesFromUrl } from '../utils';
/**
* This file exclusively deals with parsing user-defined variables
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 01cae7127e5..6306415a8b9 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -1,16 +1,16 @@
import { pickBy, mapKeys } from 'lodash';
-import {
- queryToObject,
- mergeUrlParams,
- removeParams,
- updateHistory,
-} from '~/lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
import {
timeRangeParamNames,
timeRangeFromParams,
timeRangeToParams,
} from '~/lib/utils/datetime_range';
+import {
+ queryToObject,
+ mergeUrlParams,
+ removeParams,
+ updateHistory,
+} from '~/lib/utils/url_utility';
import { VARIABLE_PREFIX } from './constants';
/**
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index fb6ef0249bb..a7696a716d0 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -1,12 +1,14 @@
import Vue from 'vue';
import store from '~/mr_notes/stores';
-import initNotesApp from './init_notes';
+import initCherryPickCommitModal from '~/projects/commit/init_cherry_pick_commit_modal';
+import initRevertCommitModal from '~/projects/commit/init_revert_commit_modal';
import initDiffsApp from '../diffs';
+import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
+import MergeRequest from '../merge_request';
import discussionCounter from '../notes/components/discussion_counter.vue';
import initDiscussionFilters from '../notes/discussion_filters';
import initSortDiscussions from '../notes/sort_discussions';
-import MergeRequest from '../merge_request';
-import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
+import initNotesApp from './init_notes';
export default function initMrNotes() {
resetServiceWorkersPublicPath();
@@ -19,6 +21,11 @@ export default function initMrNotes() {
initNotesApp();
+ document.addEventListener('merged:UpdateActions', () => {
+ initRevertCommitModal();
+ initCherryPickCommitModal();
+ });
+
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter',
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index ab88a610469..9a93e90c2bb 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
+import { parseBoolean } from '~/lib/utils/common_utils';
import store from '~/mr_notes/stores';
-import notesApp from '../notes/components/notes_app.vue';
import discussionNavigator from '../notes/components/discussion_navigator.vue';
+import notesApp from '../notes/components/notes_app.vue';
import initWidget from '../vue_merge_request_widget';
-import { parseBoolean } from '~/lib/utils/common_utils';
export default () => {
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/mr_notes/stores/index.js b/app/assets/javascripts/mr_notes/stores/index.js
index 8492b8d0aff..4a0602ad512 100644
--- a/app/assets/javascripts/mr_notes/stores/index.js
+++ b/app/assets/javascripts/mr_notes/stores/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
import batchCommentsModule from '~/batch_comments/stores/modules/batch_comments';
-import notesModule from '~/notes/stores/modules';
import diffsModule from '~/diffs/store/modules';
+import notesModule from '~/notes/stores/modules';
import mrPageModule from './modules';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue
index 2058f0c9b76..791fdf7660f 100644
--- a/app/assets/javascripts/mr_popover/components/mr_popover.vue
+++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue
@@ -3,8 +3,8 @@
import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
-import query from '../queries/merge_request.query.graphql';
import { mrStates, humanMRStates } from '../constants';
+import query from '../queries/merge_request.query.graphql';
export default {
// name: 'MRPopover' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js
index 03ddfd13d50..714cf67e0bd 100644
--- a/app/assets/javascripts/mr_popover/index.js
+++ b/app/assets/javascripts/mr_popover/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import MRPopover from './components/mr_popover.vue';
import createDefaultClient from '~/lib/graphql';
+import MRPopover from './components/mr_popover.vue';
let renderedPopover;
let renderFn;
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index e668b492ebe..af7a600d1ad 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { parseBoolean } from '~/lib/utils/common_utils';
import Api from './api';
import { mergeUrlParams } from './lib/utils/url_utility';
-import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from './locale';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class NamespaceSelect {
constructor(opts) {
diff --git a/app/assets/javascripts/namespaces/leave_by_url.js b/app/assets/javascripts/namespaces/leave_by_url.js
index 7b15253d872..094590804c1 100644
--- a/app/assets/javascripts/namespaces/leave_by_url.js
+++ b/app/assets/javascripts/namespaces/leave_by_url.js
@@ -1,7 +1,7 @@
-import { initRails } from '~/lib/utils/rails_ujs';
import { deprecatedCreateFlash as Flash } from '~/flash';
-import { __, sprintf } from '~/locale';
import { getParameterByName } from '~/lib/utils/common_utils';
+import { initRails } from '~/lib/utils/rails_ujs';
+import { __, sprintf } from '~/locale';
const PARAMETER_NAME = 'leave';
const LEAVE_LINK_SELECTOR = '.js-leave-link';
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 3574fc47088..1f360e84f68 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,8 +1,8 @@
/* eslint-disable func-names, consistent-return */
import $ from 'jquery';
-import { __ } from '../locale';
import axios from '../lib/utils/axios_utils';
+import { __ } from '../locale';
import Raphael from './raphael';
export default class BranchGraph {
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index d1d5ae5265a..e4cde0d4ff3 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
-import marked from 'marked';
import katex from 'katex';
+import marked from 'marked';
import { sanitize } from '~/lib/dompurify';
import Prompt from './prompt.vue';
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 857e5a34db6..8ed40f36103 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -10,24 +10,22 @@ class-methods-use-this */
old_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app.
*/
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import Autosize from 'autosize';
import $ from 'jquery';
-import '~/lib/utils/jquery_at_who';
-import { escape, uniqueId } from 'lodash';
import Cookies from 'js-cookie';
-import Autosize from 'autosize';
+import { escape, uniqueId } from 'lodash';
import Vue from 'vue';
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import '~/lib/utils/jquery_at_who';
import AjaxCache from '~/lib/utils/ajax_cache';
import syntaxHighlight from '~/syntax_highlight';
-import axios from './lib/utils/axios_utils';
-import { getLocationHash } from './lib/utils/url_utility';
+import Autosave from './autosave';
+import loadAwardsHandler from './awards_handler';
+import CommentTypeToggle from './comment_type_toggle';
import { deprecatedCreateFlash as Flash } from './flash';
import { defaultAutocompleteConfig } from './gfm_auto_complete';
-import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
-import loadAwardsHandler from './awards_handler';
-import Autosave from './autosave';
-import TaskList from './task_list';
+import axios from './lib/utils/axios_utils';
import {
isInViewport,
getPagePath,
@@ -36,7 +34,9 @@ import {
isInMRPage,
} from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
+import { getLocationHash } from './lib/utils/url_utility';
import { sprintf, s__, __ } from './locale';
+import TaskList from './task_list';
window.autosize = Autosize;
@@ -904,18 +904,7 @@ export default class Notes {
// DiffNote
form.find('#note_position').val(dataHolder.attr('data-position'));
- form
- .prepend(
- `<a href="${escape(
- gon.current_username,
- )}" class="user-avatar-link d-none d-sm-block"><img class="avatar s40" src="${encodeURI(
- gon.current_user_avatar_url || gon.default_avatar_url,
- )}" alt="${escape(gon.current_user_fullname)}" /></a>`,
- )
- .append('</div>')
- .find('.js-close-discussion-note-form')
- .show()
- .removeClass('hide');
+ form.append('</div>').find('.js-close-discussion-note-form').show().removeClass('hide');
form.find('.js-note-target-close').remove();
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue
index aaf64702ffd..47d14783d5d 100644
--- a/app/assets/javascripts/notes/components/comment_field_layout.vue
+++ b/app/assets/javascripts/notes/components/comment_field_layout.vue
@@ -1,6 +1,6 @@
<script>
-import EmailParticipantsWarning from './email_participants_warning.vue';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
+import EmailParticipantsWarning from './email_participants_warning.vue';
const DEFAULT_NOTEABLE_TYPE = 'Issue';
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 111af977ec5..50db3b86025 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -1,29 +1,27 @@
<script>
+import { GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
+import Autosize from 'autosize';
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
-import { isEmpty } from 'lodash';
-import Autosize from 'autosize';
-import { GlButton, GlIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import { deprecatedCreateFlash as Flash } from '~/flash';
import Autosave from '~/autosave';
+import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import {
capitalizeFirstCharacter,
convertToCamelCase,
splitCamelCase,
slugifyWithUnderscore,
} from '~/lib/utils/text_utility';
-import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import * as constants from '../constants';
-import eventHub from '../event_hub';
+import { __, sprintf } from '~/locale';
import markdownField from '~/vue_shared/components/markdown/field.vue';
-import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import noteSignedOutWidget from './note_signed_out_widget.vue';
-import discussionLockedWidget from './discussion_locked_widget.vue';
+import * as constants from '../constants';
+import eventHub from '../event_hub';
import issuableStateMixin from '../mixins/issuable_state';
import CommentFieldLayout from './comment_field_layout.vue';
+import discussionLockedWidget from './discussion_locked_widget.vue';
+import noteSignedOutWidget from './note_signed_out_widget.vue';
export default {
name: 'CommentForm',
@@ -31,11 +29,14 @@ export default {
noteSignedOutWidget,
discussionLockedWidget,
markdownField,
- userAvatarLink,
GlButton,
TimelineEntryItem,
GlIcon,
CommentFieldLayout,
+ GlFormCheckbox,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin(), issuableStateMixin],
props: {
@@ -48,8 +49,8 @@ export default {
return {
note: '',
noteType: constants.COMMENT,
+ noteIsConfidential: false,
isSubmitting: false,
- isSubmitButtonDisabled: true,
};
},
computed: {
@@ -82,6 +83,9 @@ export default {
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
},
+ canSetConfidential() {
+ return this.getNoteableData.current_user.can_update;
+ },
issueActionButtonTitle() {
const openOrClose = this.isOpen ? 'close' : 'reopen';
@@ -145,13 +149,14 @@ export default {
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
},
- },
- watch: {
- note(newNote) {
- this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
+ hasCloseAndCommentButton() {
+ return !this.glFeatures.removeCommentCloseReopen;
},
- isSubmitting(newValue) {
- this.setIsSubmitButtonDisabled(this.note, newValue);
+ confidentialNotesEnabled() {
+ return Boolean(this.glFeatures.confidentialNotes);
+ },
+ disableSubmitButton() {
+ return this.note.length === 0 || this.isSubmitting;
},
},
mounted() {
@@ -172,13 +177,6 @@ export default {
'reopenIssuable',
'toggleIssueLocalState',
]),
- setIsSubmitButtonDisabled(note, isSubmitting) {
- if (!isEmpty(note) && !isSubmitting) {
- this.isSubmitButtonDisabled = false;
- } else {
- this.isSubmitButtonDisabled = true;
- }
- },
handleSave(withIssueAction) {
if (this.note.length) {
const noteData = {
@@ -188,6 +186,7 @@ export default {
note: {
noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
+ confidential: this.noteIsConfidential,
note: this.note,
},
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
@@ -251,6 +250,7 @@ export default {
if (shouldClear) {
this.note = '';
+ this.noteIsConfidential = false;
this.resizeTextarea();
this.$refs.markdownField.previewMarkdown = false;
}
@@ -301,15 +301,6 @@ export default {
<ul v-else-if="canCreateNote" class="notes notes-form timeline">
<timeline-entry-item class="note-form">
<div class="flash-container error-alert timeline-content"></div>
- <div class="timeline-icon d-none d-md-block">
- <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 timeline-content-form">
<form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
<comment-field-layout
@@ -348,11 +339,26 @@ export default {
</markdown-field>
</comment-field-layout>
<div class="note-form-actions">
+ <gl-form-checkbox
+ v-if="confidentialNotesEnabled && canSetConfidential"
+ v-model="noteIsConfidential"
+ class="gl-mb-6"
+ data-testid="confidential-note-checkbox"
+ >
+ {{ s__('Notes|Make this comment confidential') }}
+ <gl-icon
+ v-gl-tooltip:tooltipcontainer.bottom
+ name="question"
+ :size="16"
+ :title="s__('Notes|Confidential comments are only visible to project members')"
+ class="gl-text-gray-500"
+ />
+ </gl-form-checkbox>
<div
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
<gl-button
- :disabled="isSubmitButtonDisabled"
+ :disabled="disableSubmitButton"
class="js-comment-button js-comment-submit-button"
data-qa-selector="comment_button"
data-testid="comment-button"
@@ -365,7 +371,7 @@ export default {
>{{ commentButtonTitle }}</gl-button
>
<gl-button
- :disabled="isSubmitButtonDisabled"
+ :disabled="disableSubmitButton"
name="button"
category="primary"
variant="success"
@@ -384,7 +390,7 @@ export default {
class="btn btn-transparent"
@click.prevent="setNoteType('comment')"
>
- <gl-icon name="check" class="icon" />
+ <gl-icon name="check" class="icon gl-flex-shrink-0" />
<div class="description">
<strong>{{ __('Comment') }}</strong>
<p>
@@ -400,10 +406,12 @@ export default {
<li class="divider droplab-item-ignore"></li>
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
<button
+ type="button"
+ class="btn btn-transparent"
data-qa-selector="discussion_menu_item"
@click.prevent="setNoteType('discussion')"
>
- <gl-icon name="check" class="icon" />
+ <gl-icon name="check" class="icon gl-flex-shrink-0" />
<div class="description">
<strong>{{ __('Start thread') }}</strong>
<p>{{ startDiscussionDescription }}</p>
@@ -414,7 +422,7 @@ export default {
</div>
<gl-button
- v-if="canToggleIssueState"
+ v-if="hasCloseAndCommentButton && canToggleIssueState"
:loading="isToggleStateButtonLoading"
category="secondary"
:variant="buttonVariant"
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index ee39a529345..0ce1eb8191a 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -1,10 +1,10 @@
<script>
-import { mapActions } from 'vuex';
-import { escape } from 'lodash';
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { escape } from 'lodash';
+import { mapActions } from 'vuex';
-import { s__, __, sprintf } from '~/locale';
import { truncateSha } from '~/lib/utils/text_utility';
+import { s__, __, sprintf } from '~/locale';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteEditedText from './note_edited_text.vue';
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index b7355d4d927..e96e1204f76 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,12 +1,12 @@
<script>
/* eslint-disable vue/no-v-html */
-import { mapState, mapActions } from 'vuex';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
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 { getDiffMode } from '~/diffs/store/utils';
import { diffViewerModes } from '~/ide/constants';
+import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { isCollapsed } from '../../diffs/utils/diff_file';
const FIRST_CHAR_REGEX = /^(\+|-| )/;
diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue
index da4134ab2c4..27408bc3354 100644
--- a/app/assets/javascripts/notes/components/discussion_actions.vue
+++ b/app/assets/javascripts/notes/components/discussion_actions.vue
@@ -1,8 +1,8 @@
<script>
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReplyPlaceholder from './discussion_reply_placeholder.vue';
import ResolveDiscussionButton from './discussion_resolve_button.vue';
import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'DiscussionActions',
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 0a72627834d..55cf75132a9 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import { GlTooltipDirective, GlIcon, GlButton, GlButtonGroup } from '@gitlab/ui';
+import { mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
import discussionNavigation from '../mixins/discussion_navigation';
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index aa61aa9b3cb..88f053aed67 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
+import { mapGetters, mapActions } from 'vuex';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import {
DISCUSSION_FILTERS_DEFAULT_VALUE,
diff --git a/app/assets/javascripts/notes/components/discussion_navigator.vue b/app/assets/javascripts/notes/components/discussion_navigator.vue
index facc53e27a6..fa3c900c337 100644
--- a/app/assets/javascripts/notes/components/discussion_navigator.vue
+++ b/app/assets/javascripts/notes/components/discussion_navigator.vue
@@ -1,8 +1,8 @@
<script>
/* global Mousetrap */
import 'mousetrap';
-import discussionNavigation from '~/notes/mixins/discussion_navigation';
import eventHub from '~/notes/event_hub';
+import discussionNavigation from '~/notes/mixins/discussion_navigation';
export default {
mixins: [discussionNavigation],
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 8ac915c3c03..0f74d78c8e0 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -1,15 +1,14 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { SYSTEM_NOTE } from '../constants';
import { __ } from '~/locale';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
+import { SYSTEM_NOTE } from '../constants';
+import DiscussionNotesRepliesWrapper from './discussion_notes_replies_wrapper.vue';
+import NoteEditedText from './note_edited_text.vue';
import NoteableNote from './noteable_note.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
-import NoteEditedText from './note_edited_text.vue';
-import DiscussionNotesRepliesWrapper from './discussion_notes_replies_wrapper.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'DiscussionNotes',
@@ -18,7 +17,6 @@ export default {
NoteEditedText,
DiscussionNotesRepliesWrapper,
},
- mixins: [glFeatureFlagsMixin()],
props: {
discussion: {
type: Object,
@@ -96,14 +94,14 @@ export default {
return note.isPlaceholderNote ? note.notes[0] : note;
},
handleMouseEnter(discussion) {
- if (this.glFeatures.multilineComments && discussion.position) {
+ if (discussion.position) {
this.setSelectedCommentPositionHover(discussion.position.line_range);
}
},
handleMouseLeave(discussion) {
- // Even though position isn't used here we still don't want to unecessarily call a mutation
+ // Even though position isn't used here we still don't want to unnecessarily call a mutation
// The lack of position tells us that highlighting is irrelevant in this context
- if (this.glFeatures.multilineComments && discussion.position) {
+ if (discussion.position) {
this.setSelectedCommentPositionHover();
}
},
diff --git a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue
index 2ddca56ddd5..dfeda4aae7c 100644
--- a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue
@@ -16,9 +16,14 @@ export default {
},
render(h, { props, children }) {
if (props.isDiffDiscussion) {
- return h('li', { class: 'discussion-collapsible bordered-box clearfix' }, [
- h('ul', { class: 'notes' }, children),
- ]);
+ return h(
+ 'li',
+ {
+ class:
+ 'discussion-collapsible gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base gl-overflow-hidden clearfix',
+ },
+ [h('ul', { class: 'notes' }, children)],
+ );
}
return children;
diff --git a/app/assets/javascripts/notes/components/email_participants_warning.vue b/app/assets/javascripts/notes/components/email_participants_warning.vue
index bb1ff58120a..ecf42fce1d2 100644
--- a/app/assets/javascripts/notes/components/email_participants_warning.vue
+++ b/app/assets/javascripts/notes/components/email_participants_warning.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
import { toNounSeriesText } from '~/lib/utils/grammar';
+import { s__, sprintf } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue
index 9fbf2c9265c..6ad565567be 100644
--- a/app/assets/javascripts/notes/components/multiline_comment_form.vue
+++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlFormSelect, GlSprintf } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { getSymbol, getLineClasses } from './multiline_comment_utils';
export default {
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index b85cfa83e09..907a4316a93 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,13 +1,14 @@
<script>
-import { mapGetters } from 'vuex';
import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
-import ReplyButton from './note_actions/reply_button.vue';
-import eventHub from '~/sidebar/event_hub';
+import { mapGetters } from 'vuex';
import Api from '~/api';
+import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import { __, sprintf } from '~/locale';
+import eventHub from '~/sidebar/event_hub';
import { splitCamelCase } from '../../lib/utils/text_utility';
+import ReplyButton from './note_actions/reply_button.vue';
export default {
name: 'NoteActions',
@@ -193,7 +194,7 @@ export default {
},
closeTooltip() {
this.$nextTick(() => {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
});
},
handleAssigneeUpdate(assignees) {
@@ -243,66 +244,62 @@ export default {
:title="displayContributorBadgeText"
>{{ __('Contributor') }}</span
>
- <div v-if="canResolve" class="gl-ml-2">
- <gl-button
- ref="resolveButton"
- v-gl-tooltip
- size="small"
- category="tertiary"
- :variant="resolveVariant"
- :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
- :title="resolveButtonTitle"
- :aria-label="resolveButtonTitle"
- :icon="resolveIcon"
- :loading="isResolving"
- class="line-resolve-btn note-action-button"
- @click="onResolve"
- />
- </div>
- <div v-if="canAwardEmoji" class="gl-ml-3 gl-mr-2">
- <a
- v-gl-tooltip
- :class="{ 'js-user-authored': isAuthoredByCurrentUser }"
- class="note-action-button note-emoji-button js-add-award js-note-emoji"
- href="#"
- title="Add reaction"
- data-position="right"
- >
- <gl-icon class="link-highlight award-control-icon-neutral" name="slight-smile" />
- <gl-icon class="link-highlight award-control-icon-positive" name="smiley" />
- <gl-icon class="link-highlight award-control-icon-super-positive" name="smile" />
- </a>
- </div>
+ <gl-button
+ v-if="canResolve"
+ ref="resolveButton"
+ v-gl-tooltip
+ size="small"
+ category="tertiary"
+ :variant="resolveVariant"
+ :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
+ :title="resolveButtonTitle"
+ :aria-label="resolveButtonTitle"
+ :icon="resolveIcon"
+ :loading="isResolving"
+ class="line-resolve-btn note-action-button"
+ @click="onResolve"
+ />
+ <a
+ v-if="canAwardEmoji"
+ v-gl-tooltip
+ :class="{ 'js-user-authored': isAuthoredByCurrentUser }"
+ class="note-action-button note-emoji-button js-add-award js-note-emoji gl-text-gray-600 gl-m-2"
+ href="#"
+ title="Add reaction"
+ data-position="right"
+ >
+ <gl-icon class="link-highlight award-control-icon-neutral" name="slight-smile" />
+ <gl-icon class="link-highlight award-control-icon-positive" name="smiley" />
+ <gl-icon class="link-highlight award-control-icon-super-positive" name="smile" />
+ </a>
<reply-button
v-if="showReply"
ref="replyButton"
class="js-reply-button"
@startReplying="$emit('startReplying')"
/>
- <div v-if="canEdit" class="gl-ml-2">
- <gl-button
- v-gl-tooltip
- title="Edit comment"
- icon="pencil"
- size="small"
- category="tertiary"
- class="note-action-button js-note-edit btn btn-transparent"
- data-qa-selector="note_edit_button"
- @click="onEdit"
- />
- </div>
- <div v-if="showDeleteAction" class="gl-ml-2">
- <gl-button
- v-gl-tooltip
- title="Delete comment"
- size="small"
- icon="remove"
- category="tertiary"
- class="note-action-button js-note-delete btn btn-transparent"
- @click="onDelete"
- />
- </div>
- <div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions gl-ml-2">
+ <gl-button
+ v-if="canEdit"
+ v-gl-tooltip
+ title="Edit comment"
+ icon="pencil"
+ size="small"
+ category="tertiary"
+ class="note-action-button js-note-edit btn btn-transparent"
+ data-qa-selector="note_edit_button"
+ @click="onEdit"
+ />
+ <gl-button
+ v-if="showDeleteAction"
+ v-gl-tooltip
+ title="Delete comment"
+ size="small"
+ icon="remove"
+ category="tertiary"
+ class="note-action-button js-note-delete btn btn-transparent"
+ @click="onDelete"
+ />
+ <div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions">
<gl-button
v-gl-tooltip
title="More actions"
diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
index acbbee13a6d..5ce03091504 100644
--- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue
+++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
@@ -1,7 +1,11 @@
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
export default {
+ i18n: {
+ buttonText: __('Reply to comment'),
+ },
name: 'ReplyButton',
components: {
GlButton,
@@ -13,18 +17,15 @@ export default {
</script>
<template>
- <div class="gl-ml-2">
- <gl-button
- ref="button"
- v-gl-tooltip
- data-track-event="click_button"
- data-track-label="reply_comment_button"
- category="tertiary"
- size="small"
- icon="comment"
- :title="__('Reply to comment')"
- :aria-label="__('Reply to comment')"
- @click="$emit('startReplying')"
- />
- </div>
+ <gl-button
+ v-gl-tooltip
+ data-track-event="click_button"
+ data-track-label="reply_comment_button"
+ category="tertiary"
+ size="small"
+ icon="comment"
+ :title="$options.i18n.buttonText"
+ :aria-label="$options.i18n.buttonText"
+ @click="$emit('startReplying')"
+ />
</template>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index cf3e991986c..9eb7b928ea4 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import { __ } from '~/locale';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
-import { __ } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 8855ceac3d5..8c5d81c0cc9 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,14 +1,16 @@
<script>
/* eslint-disable vue/no-v-html */
-import { mapActions, mapGetters, mapState } from 'vuex';
import $ from 'jquery';
+import { escape } from 'lodash';
+import { mapActions, mapGetters, mapState } from 'vuex';
+
import '~/behaviors/markdown/render_gfm';
-import noteEditedText from './note_edited_text.vue';
-import noteAwardsList from './note_awards_list.vue';
+import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
+import autosave from '../mixins/autosave';
import noteAttachment from './note_attachment.vue';
+import noteAwardsList from './note_awards_list.vue';
+import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
-import autosave from '../mixins/autosave';
-import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
export default {
components: {
@@ -29,6 +31,11 @@ export default {
required: false,
default: null,
},
+ file: {
+ type: Object,
+ required: false,
+ default: null,
+ },
canEdit: {
type: Boolean,
required: true,
@@ -46,6 +53,7 @@ export default {
},
computed: {
...mapGetters(['getDiscussion', 'suggestionsCount']),
+ ...mapGetters('diffs', ['suggestionCommitMessage']),
discussion() {
if (!this.note.isDraft) return {};
@@ -54,7 +62,6 @@ export default {
...mapState({
batchSuggestionsInfo: (state) => state.notes.batchSuggestionsInfo,
}),
- ...mapState('diffs', ['defaultSuggestionCommitMessage']),
noteBody() {
return this.note.note;
},
@@ -64,6 +71,21 @@ export default {
lineType() {
return this.line ? this.line.type : null;
},
+ commitMessage() {
+ // Please see this issue comment for why these
+ // are hard-coded to 1:
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/291027#note_468308022
+ const suggestionsCount = 1;
+ const filesCount = 1;
+ const filePaths = this.file ? [this.file.file_path] : [];
+ const suggestion = this.suggestionCommitMessage({
+ file_paths: filePaths.join(', '),
+ suggestions_count: suggestionsCount,
+ files_count: filesCount,
+ });
+
+ return escape(suggestion);
+ },
},
mounted() {
this.renderGFM();
@@ -135,7 +157,7 @@ export default {
:note-html="note.note_html"
:line-type="lineType"
:help-page-path="helpPagePath"
- :default-commit-message="defaultSuggestionCommitMessage"
+ :default-commit-message="commitMessage"
@apply="applySuggestion"
@applyBatch="applySuggestionBatch"
@addToBatch="addSuggestionToBatch"
@@ -156,6 +178,7 @@ export default {
@handleFormUpdate="handleFormUpdate"
@cancelForm="formCancelHandler"
/>
+ <!-- eslint-disable vue/no-mutating-props -->
<textarea
v-if="canEdit"
v-model="note.note"
@@ -163,6 +186,7 @@ export default {
class="hidden js-task-list-field"
dir="auto"
></textarea>
+ <!-- eslint-enable vue/no-mutating-props -->
<note-edited-text
v-if="note.last_edited_at"
:edited-at="note.last_edited_at"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 9acb837c27f..653bc450d0b 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,14 +1,15 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlButton } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
+import { getDraft, updateDraft } from '~/lib/utils/autosave';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import eventHub from '../event_hub';
+import { __, sprintf } from '~/locale';
import markdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import eventHub from '../event_hub';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
-import { __, sprintf } from '~/locale';
-import { getDraft, updateDraft } from '~/lib/utils/autosave';
import CommentFieldLayout from './comment_field_layout.vue';
export default {
@@ -16,6 +17,7 @@ export default {
components: {
markdownField,
CommentFieldLayout,
+ GlButton,
},
mixins: [glFeatureFlagsMixin(), issuableStateMixin, resolvable],
props: {
@@ -378,61 +380,70 @@ export default {
</template>
</label>
</p>
- <div>
- <button
+ <div class="gl-display-sm-flex gl-flex-wrap">
+ <gl-button
:disabled="isDisabled"
- type="button"
- class="btn btn-success"
+ category="primary"
+ variant="success"
+ class="gl-mr-3"
data-qa-selector="start_review_button"
@click="handleAddToReview"
>
<template v-if="hasDrafts">{{ __('Add to review') }}</template>
<template v-else>{{ __('Start a review') }}</template>
- </button>
- <button
+ </gl-button>
+ <gl-button
:disabled="isDisabled"
- type="button"
- class="btn js-comment-button"
+ category="secondary"
+ variant="default"
data-qa-selector="comment_now_button"
+ class="gl-mr-3 js-comment-button"
@click="handleUpdate()"
>
{{ __('Add comment now') }}
- </button>
- <button
- class="btn note-edit-cancel js-close-discussion-note-form"
- type="button"
+ </gl-button>
+ <gl-button
+ class="note-edit-cancel js-close-discussion-note-form"
+ category="secondary"
+ variant="default"
data-testid="cancelBatchCommentsEnabled"
@click="cancelHandler(true)"
>
{{ __('Cancel') }}
- </button>
+ </gl-button>
</div>
</template>
<template v-else>
- <button
- :disabled="isDisabled"
- type="button"
- class="js-vue-issue-save btn btn-success js-comment-button"
- data-qa-selector="reply_comment_button"
- @click="handleUpdate()"
- >
- {{ saveButtonTitle }}
- </button>
- <button
- v-if="discussion.resolvable"
- class="btn btn-default gl-mr-3 js-comment-resolve-button"
- @click.prevent="handleUpdate(true)"
- >
- {{ resolveButtonTitle }}
- </button>
- <button
- class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
- type="button"
- data-testid="cancel"
- @click="cancelHandler(true)"
- >
- {{ __('Cancel') }}
- </button>
+ <div class="gl-display-sm-flex gl-flex-wrap">
+ <gl-button
+ :disabled="isDisabled"
+ category="primary"
+ variant="success"
+ data-qa-selector="reply_comment_button"
+ class="gl-mr-3 js-vue-issue-save js-comment-button"
+ @click="handleUpdate()"
+ >
+ {{ saveButtonTitle }}
+ </gl-button>
+ <gl-button
+ v-if="discussion.resolvable"
+ category="secondary"
+ variant="default"
+ class="gl-mr-3 js-comment-resolve-button"
+ @click.prevent="handleUpdate(true)"
+ >
+ {{ resolveButtonTitle }}
+ </gl-button>
+ <gl-button
+ class="note-edit-cancel js-close-discussion-note-form"
+ category="secondary"
+ variant="default"
+ data-testid="cancel"
+ @click="cancelHandler(true)"
+ >
+ {{ __('Cancel') }}
+ </gl-button>
+ </div>
</template>
</div>
</form>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 17a995018d3..6932af61c69 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,9 +1,9 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
-import { GlIcon, GlLoadingIcon, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
-import { isUserBusy } from '~/set_status_modal/utils';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserNameWithStatus from '../../sidebar/components/assignees/user_name_with_status.vue';
export default {
components: {
@@ -12,7 +12,7 @@ export default {
import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'),
GlIcon,
GlLoadingIcon,
- GlSprintf,
+ UserNameWithStatus,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -90,10 +90,6 @@ export default {
}
return false;
},
- authorIsBusy() {
- const { status } = this.author;
- return status?.availability && isUserBusy(status.availability);
- },
emojiElement() {
return this.$refs?.authorStatus?.querySelector('gl-emoji');
},
@@ -133,6 +129,9 @@ export default {
this.$refs.authorNameLink.dispatchEvent(new Event('mouseleave'));
this.isUsernameLinkHovered = false;
},
+ userAvailability(selectedAuthor) {
+ return selectedAuthor?.availability || '';
+ },
},
};
</script>
@@ -158,12 +157,11 @@ export default {
:data-username="author.username"
>
<slot name="note-header-info"></slot>
- <span class="note-header-author-name gl-font-weight-bold">
- <gl-sprintf v-if="authorIsBusy" :message="s__('UserAvailability|%{author} (Busy)')">
- <template #author>{{ authorName }}</template>
- </gl-sprintf>
- <template v-else>{{ authorName }}</template>
- </span>
+ <user-name-with-status
+ :name="authorName"
+ :availability="userAvailability(author)"
+ container-classes="note-header-author-name gl-font-weight-bold"
+ />
</a>
<span
v-if="authorStatus"
@@ -210,9 +208,9 @@ export default {
v-gl-tooltip:tooltipcontainer.bottom
data-testid="confidentialIndicator"
name="eye-slash"
- :size="14"
- :title="s__('Notes|Private comments are accessible by internal staff only')"
- class="gl-ml-1 gl-text-gray-700 align-middle"
+ :size="16"
+ :title="s__('Notes|This comment is confidential and only visible to project members')"
+ class="gl-ml-1 gl-text-orange-700 align-middle"
/>
<slot name="extra-controls"></slot>
<gl-loading-icon
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 0a9a3da6069..34dd21dcbac 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,22 +1,22 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
-import { s__, __ } from '~/locale';
+import { mapActions, mapGetters } from 'vuex';
+import DraftNote from '~/batch_comments/components/draft_note.vue';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
+import { s__, __ } from '~/locale';
+import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import DraftNote from '~/batch_comments/components/draft_note.vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import diffDiscussionHeader from './diff_discussion_header.vue';
-import noteSignedOutWidget from './note_signed_out_widget.vue';
-import noteForm from './note_form.vue';
-import diffWithNote from './diff_with_note.vue';
+import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
-import eventHub from '../event_hub';
-import DiscussionNotes from './discussion_notes.vue';
+import diffDiscussionHeader from './diff_discussion_header.vue';
+import diffWithNote from './diff_with_note.vue';
import DiscussionActions from './discussion_actions.vue';
+import DiscussionNotes from './discussion_notes.vue';
+import noteForm from './note_form.vue';
+import noteSignedOutWidget from './note_signed_out_widget.vue';
export default {
name: 'NoteableDiscussion',
@@ -265,16 +265,8 @@ export default {
<div
v-else-if="showReplies"
:class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder clearfix"
+ class="discussion-reply-holder gl-border-t-0! clearfix"
>
- <user-avatar-link
- v-if="!isReplying && userCanReply"
- :link-href="currentUser.path"
- :img-src="currentUser.avatar_url"
- :img-alt="currentUser.name"
- :img-size="40"
- class="d-none d-sm-block"
- />
<discussion-actions
v-if="!isReplying && userCanReply"
:discussion="discussion"
@@ -285,27 +277,18 @@ export default {
@showReplyForm="showReplyForm"
@resolve="resolveHandler"
/>
- <div v-if="isReplying" class="avatar-note-form-holder">
- <user-avatar-link
- v-if="currentUser"
- :link-href="currentUser.path"
- :img-src="currentUser.avatar_url"
- :img-alt="currentUser.name"
- :img-size="40"
- class="d-none d-sm-block"
- />
- <note-form
- ref="noteForm"
- :discussion="discussion"
- :is-editing="false"
- :line="diffLine"
- save-button-title="Comment"
- :autosave-key="autosaveKey"
- @handleFormUpdateAddToReview="addReplyToReview"
- @handleFormUpdate="saveReply"
- @cancelForm="cancelReplyForm"
- />
- </div>
+ <note-form
+ v-if="isReplying"
+ ref="noteForm"
+ :discussion="discussion"
+ :is-editing="false"
+ :line="diffLine"
+ save-button-title="Comment"
+ :autosave-key="autosaveKey"
+ @handleFormUpdateAddToReview="addReplyToReview"
+ @handleFormUpdate="saveReply"
+ @cancelForm="cancelReplyForm"
+ />
<note-signed-out-widget v-if="!isLoggedIn" />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index eaa64cf7c01..4343fac3cfa 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -1,21 +1,18 @@
<script>
+import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import $ from 'jquery';
-import { mapGetters, mapActions } from 'vuex';
import { escape } from 'lodash';
-import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { mapGetters, mapActions } from 'vuex';
+import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
+import httpStatusCodes from '~/lib/utils/http_status';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import { __, s__, sprintf } from '../../locale';
import { deprecatedCreateFlash as Flash } from '../../flash';
+import { __, s__, sprintf } from '../../locale';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import noteHeader from './note_header.vue';
-import noteActions from './note_actions.vue';
-import NoteBody from './note_body.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
-import httpStatusCodes from '~/lib/utils/http_status';
import {
getStartLineNumber,
getEndLineNumber,
@@ -23,7 +20,9 @@ import {
commentLineOptions,
formatLineRange,
} from './multiline_comment_utils';
-import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
+import noteActions from './note_actions.vue';
+import NoteBody from './note_body.vue';
+import noteHeader from './note_header.vue';
export default {
name: 'NoteableNote',
@@ -38,7 +37,7 @@ export default {
directives: {
SafeHtml,
},
- mixins: [noteable, resolvable, glFeatureFlagsMixin()],
+ mixins: [noteable, resolvable],
props: {
note: {
type: Object,
@@ -160,7 +159,6 @@ export default {
},
showMultiLineComment() {
if (
- !this.glFeatures.multilineComments ||
!this.discussionRoot ||
this.startLineNumber.length === 0 ||
this.endLineNumber.length === 0
@@ -289,6 +287,7 @@ export default {
};
this.isRequesting = true;
this.oldContent = this.note.note_html;
+ // eslint-disable-next-line vue/no-mutating-props
this.note.note_html = escape(noteText);
this.updateNote(data)
@@ -321,6 +320,7 @@ export default {
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
+ // eslint-disable-next-line vue/no-mutating-props
this.note.note_html = this.oldContent;
this.oldContent = null;
}
@@ -330,6 +330,7 @@ export default {
recoverNoteContent(noteText) {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
+ // eslint-disable-next-line vue/no-mutating-props
this.note.note = noteText;
const { noteBody } = this.$refs;
if (noteBody) {
@@ -428,6 +429,7 @@ export default {
ref="noteBody"
:note="note"
:line="line"
+ :file="diffFile"
:can-edit="note.current_user.can_edit"
:is-editing="isEditing"
:help-page-path="helpPagePath"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index e9e687a8743..2d66e0d24e3 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,21 +1,22 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
+import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
+import { __ } from '~/locale';
+import initUserPopovers from '~/user_popovers';
+import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as Flash } from '../../flash';
-import * as constants from '../constants';
-import eventHub from '../event_hub';
-import noteableNote from './noteable_note.vue';
-import noteableDiscussion from './noteable_discussion.vue';
-import discussionFilterNote from './discussion_filter_note.vue';
-import systemNote from '../../vue_shared/components/notes/system_note.vue';
-import commentForm from './comment_form.vue';
+import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
-import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
-import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
-import { __ } from '~/locale';
-import initUserPopovers from '~/user_popovers';
+import systemNote from '../../vue_shared/components/notes/system_note.vue';
+import * as constants from '../constants';
+import eventHub from '../event_hub';
+import commentForm from './comment_form.vue';
+import discussionFilterNote from './discussion_filter_note.vue';
+import noteableDiscussion from './noteable_discussion.vue';
+import noteableNote from './noteable_note.vue';
export default {
name: 'NotesApp',
@@ -30,6 +31,7 @@ export default {
discussionFilterNote,
OrderedLayout,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
noteableData: {
type: Object,
@@ -57,7 +59,6 @@ export default {
},
data() {
return {
- isFetching: false,
currentFilter: null,
};
},
@@ -68,6 +69,7 @@ export default {
'convertedDisscussionIds',
'getNotesDataByProp',
'isLoading',
+ 'isFetching',
'commentsDisabled',
'getNoteableData',
'userCanReply',
@@ -103,6 +105,13 @@ export default {
},
},
watch: {
+ async isFetching() {
+ if (!this.isFetching) {
+ await this.$nextTick();
+ await this.startTaskList();
+ await this.checkLocationHash();
+ }
+ },
shouldShow() {
if (!this.isNotesFetched) {
this.fetchNotes();
@@ -153,6 +162,7 @@ export default {
},
methods: {
...mapActions([
+ 'setFetchingState',
'setLoadingState',
'fetchDiscussions',
'poll',
@@ -183,7 +193,11 @@ export default {
fetchNotes() {
if (this.isFetching) return null;
- this.isFetching = true;
+ this.setFetchingState(true);
+
+ if (this.glFeatures.paginatedNotes) {
+ return this.initPolling();
+ }
return this.fetchDiscussions(this.getFetchDiscussionsConfig())
.then(this.initPolling)
@@ -191,11 +205,8 @@ export default {
this.setLoadingState(false);
this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
- this.isFetching = false;
+ this.setFetchingState(false);
})
- .then(this.$nextTick)
- .then(this.startTaskList)
- .then(this.checkLocationHash)
.catch(() => {
this.setLoadingState(false);
this.setNotesFetchedState(true);
diff --git a/app/assets/javascripts/notes/components/sort_discussion.vue b/app/assets/javascripts/notes/components/sort_discussion.vue
index c279a7107c7..ed1f456c174 100644
--- a/app/assets/javascripts/notes/components/sort_discussion.vue
+++ b/app/assets/javascripts/notes/components/sort_discussion.vue
@@ -2,8 +2,8 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import { __ } from '~/locale';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import Tracking from '~/tracking';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { ASC, DESC } from '../constants';
const SORT_OPTIONS = [
diff --git a/app/assets/javascripts/notes/components/timeline_toggle.vue b/app/assets/javascripts/notes/components/timeline_toggle.vue
index 8162878f80d..87d22e5b986 100644
--- a/app/assets/javascripts/notes/components/timeline_toggle.vue
+++ b/app/assets/javascripts/notes/components/timeline_toggle.vue
@@ -2,9 +2,9 @@
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
import { COMMENTS_ONLY_FILTER_VALUE, DESC } from '../constants';
import notesEventHub from '../event_hub';
-import TrackEventDirective from '~/vue_shared/directives/track_event';
import { trackToggleTimelineView } from '../utils';
export const timelineEnabledTooltip = s__('Timeline|Turn timeline view off');
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index ab7fa793bdc..01e3f84d00e 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -1,8 +1,8 @@
<script>
-import { uniqBy } from 'lodash';
import { GlButton, GlIcon } from '@gitlab/ui';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { uniqBy } from 'lodash';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
components: {
@@ -39,13 +39,17 @@ export default {
this.$emit('toggle');
},
},
+ ICON_CLASS: 'gl-mr-3 gl-cursor-pointer',
};
</script>
<template>
- <li :class="className" class="replies-toggle js-toggle-replies">
+ <li
+ :class="className"
+ class="replies-toggle js-toggle-replies gl-display-flex! gl-align-items-center gl-flex-wrap"
+ >
<template v-if="collapsed">
- <gl-icon name="chevron-right" @click.native="toggle" />
+ <gl-icon :class="$options.ICON_CLASS" name="chevron-right" @click.native="toggle" />
<div>
<user-avatar-link
v-for="author in uniqueAuthors"
@@ -59,7 +63,7 @@ export default {
/>
</div>
<gl-button
- class="js-replies-text"
+ class="js-replies-text gl-mr-2"
category="tertiary"
variant="link"
data-qa-selector="expand_replies_button"
@@ -68,18 +72,19 @@ export default {
{{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
</gl-button>
{{ __('Last reply by') }}
- <a :href="lastReply.author.path" class="btn btn-link author-link">
+ <a :href="lastReply.author.path" class="btn btn-link author-link gl-mx-2">
{{ lastReply.author.name }}
</a>
<time-ago-tooltip :time="lastReply.created_at" tooltip-placement="bottom" />
</template>
- <span
+ <div
v-else
- class="collapse-replies-btn js-collapse-replies"
+ class="collapse-replies-btn js-collapse-replies gl-display-flex align-items-center"
data-qa-selector="collapse_replies_button"
@click="toggle"
>
- <gl-icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }}
- </span>
+ <gl-icon :class="$options.ICON_CLASS" name="chevron-down" />
+ <span class="gl-cursor-pointer">{{ s__('Notes|Collapse replies') }}</span>
+ </div>
</li>
</template>
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 1f0b2afab9e..e4241669fbc 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -2,8 +2,8 @@ import Vue from 'vue';
import notesApp from './components/notes_app.vue';
import initDiscussionFilters from './discussion_filters';
import initSortDiscussions from './sort_discussions';
-import initTimelineToggle from './timeline';
import { store } from './stores';
+import initTimelineToggle from './timeline';
const el = document.getElementById('js-vue-notes');
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index b161773f5f1..d670d0bd4c5 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import { s__ } from '~/locale';
import Autosave from '../../autosave';
import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
-import { s__ } from '~/locale';
export default {
methods: {
diff --git a/app/assets/javascripts/notes/mixins/diff_line_note_form.js b/app/assets/javascripts/notes/mixins/diff_line_note_form.js
index 5ce541781d4..76342e07c04 100644
--- a/app/assets/javascripts/notes/mixins/diff_line_note_form.js
+++ b/app/assets/javascripts/notes/mixins/diff_line_note_form.js
@@ -2,8 +2,8 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import { getDraftReplyFormData, getDraftFormData } from '~/batch_comments/utils';
import { TEXT_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__ } from '~/locale';
import { clearDraft } from '~/lib/utils/autosave';
+import { s__ } from '~/locale';
import { formatLineRange } from '~/notes/components/multiline_comment_utils';
export default {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index c6684efed4d..19403c29cda 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,23 +1,24 @@
-import Vue from 'vue';
import $ from 'jquery';
import Visibility from 'visibilityjs';
+import Vue from 'vue';
+import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
-import TaskList from '../../task_list';
-import { deprecatedCreateFlash as Flash } from '../../flash';
-import Poll from '../../lib/utils/poll';
-import * as types from './mutation_types';
-import * as utils from './utils';
-import * as constants from '../constants';
+import { __, sprintf } from '~/locale';
+import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
+import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
+import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
import loadAwardsHandler from '../../awards_handler';
-import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
+import Poll from '../../lib/utils/poll';
import { mergeUrlParams } from '../../lib/utils/url_utility';
+import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
+import TaskList from '../../task_list';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
-import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
-import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
-import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
-import { __, sprintf } from '~/locale';
-import Api from '~/api';
+import * as constants from '../constants';
+import eventHub from '../event_hub';
+import * as types from './mutation_types';
+import * as utils from './utils';
let eTagPoll;
@@ -420,14 +421,25 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
.catch(processErrors);
};
-const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
+export const setFetchingState = ({ commit }, fetchingState) =>
+ commit(types.SET_NOTES_FETCHING_STATE, fetchingState);
+
+const pollSuccessCallBack = async (resp, commit, state, getters, dispatch) => {
if (state.isResolvingDiscussion) {
return null;
}
+ if (window.gon?.features?.paginatedNotes && !resp.more && state.isFetching) {
+ eventHub.$emit('fetchedNotesData');
+ dispatch('setFetchingState', false);
+ dispatch('setNotesFetchedState', true);
+ dispatch('setLoadingState', false);
+ }
+
if (resp.notes?.length) {
- dispatch('updateOrCreateNotes', resp.notes);
+ await dispatch('updateOrCreateNotes', resp.notes);
dispatch('startTaskList');
+ dispatch('updateResolvableDiscussionsCounts');
}
commit(types.SET_LAST_FETCHED_AT, resp.last_fetched_at);
@@ -727,9 +739,13 @@ export const updateConfidentialityOnIssuable = (
})
.then(({ data }) => {
const {
- issueSetConfidential: { issue },
+ issueSetConfidential: { issue, errors },
} = data;
- setConfidentiality({ commit }, issue.confidential);
+ if (errors?.length) {
+ Flash(errors[0], 'alert');
+ } else {
+ setConfidentiality({ commit }, issue.confidential);
+ }
});
};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 5891a2e63e3..43d99937b8d 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -48,6 +48,8 @@ export const persistSortOrder = (state) => state.persistSortOrder;
export const timelineEnabled = (state) => state.isTimelineEnabled;
+export const isFetching = (state) => state.isFetching;
+
export const isLoading = (state) => state.isLoading;
export const getNotesDataByProp = (state) => (prop) => state.notesData[prop];
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 144a3d7ba90..f154edd3434 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -1,7 +1,7 @@
+import { ASC } from '../../constants';
import * as actions from '../actions';
import * as getters from '../getters';
import mutations from '../mutations';
-import { ASC } from '../../constants';
export default () => ({
state: {
@@ -47,6 +47,7 @@ export default () => ({
unresolvedDiscussionsCount: 0,
descriptionVersions: {},
isTimelineEnabled: false,
+ isFetching: false,
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 5c4f62f4575..2e8b728e013 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const UPDATE_DISCUSSION_POSITION = 'UPDATE_DISCUSSION_POSITION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
+export const SET_NOTES_FETCHING_STATE = 'SET_NOTES_FETCHING_STATE';
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';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 2c51ce0d970..c5fa34dfedd 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -1,7 +1,8 @@
-import * as utils from './utils';
-import * as types from './mutation_types';
-import * as constants from '../constants';
+import { isEqual } from 'lodash';
import { isInMRPage } from '../../lib/utils/common_utils';
+import * as constants from '../constants';
+import * as types from './mutation_types';
+import * as utils from './utils';
export default {
[types.ADD_NEW_NOTE](state, data) {
@@ -31,7 +32,22 @@ export default {
}
}
- note.base_discussion = undefined; // No point keeping a reference to this
+ if (window.gon?.features?.paginatedNotes && note.base_discussion) {
+ if (discussion.diff_file) {
+ discussion.file_hash = discussion.diff_file.file_hash;
+
+ discussion.truncated_diff_lines = utils.prepareDiffLines(
+ discussion.truncated_diff_lines || [],
+ );
+ }
+
+ discussion.resolvable = note.resolvable;
+ discussion.expanded = note.base_discussion.expanded;
+ discussion.resolved = note.resolved;
+ }
+
+ // note.base_discussion = undefined; // No point keeping a reference to this
+ delete note.base_discussion;
discussion.notes = [note];
state.discussions.push(discussion);
@@ -220,6 +236,11 @@ export default {
[types.UPDATE_NOTE](state, note) {
const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
+ // Disable eslint here so we can delete the property that we no longer need
+ // in the note object
+ // eslint-disable-next-line no-param-reassign
+ delete note.base_discussion;
+
if (noteObj.individual_note) {
if (note.type === constants.DISCUSSION_NOTE) {
noteObj.individual_note = false;
@@ -228,7 +249,10 @@ export default {
noteObj.notes.splice(0, 1, note);
} else {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
- noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
+
+ if (!isEqual(comment, note)) {
+ noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
+ }
}
},
@@ -313,6 +337,10 @@ export default {
state.isLoading = value;
},
+ [types.SET_NOTES_FETCHING_STATE](state, value) {
+ state.isFetching = value;
+ },
+
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index 6df926e1249..627e405c75c 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -1,7 +1,7 @@
-import AjaxCache from '~/lib/utils/ajax_cache';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
-import { sprintf, __ } from '~/locale';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import { sprintf, __ } from '~/locale';
// factory function because global flag makes RegExp stateful
const createQuickActionsRegex = () => /^\/\w+.*$/gm;
diff --git a/app/assets/javascripts/notifications/components/custom_notifications_modal.vue b/app/assets/javascripts/notifications/components/custom_notifications_modal.vue
new file mode 100644
index 00000000000..0f628897e17
--- /dev/null
+++ b/app/assets/javascripts/notifications/components/custom_notifications_modal.vue
@@ -0,0 +1,128 @@
+<script>
+import { GlModal, GlSprintf, GlLink, GlLoadingIcon, GlFormGroup, GlFormCheckbox } from '@gitlab/ui';
+import Api from '~/api';
+import { i18n } from '../constants';
+
+export default {
+ name: 'CustomNotificationsModal',
+ components: {
+ GlModal,
+ GlSprintf,
+ GlLink,
+ GlLoadingIcon,
+ GlFormGroup,
+ GlFormCheckbox,
+ },
+ inject: {
+ projectId: {
+ default: null,
+ },
+ groupId: {
+ default: null,
+ },
+ helpPagePath: {
+ default: '',
+ },
+ },
+ props: {
+ modalId: {
+ type: String,
+ required: false,
+ default: 'custom-notifications-modal',
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ events: [],
+ };
+ },
+ methods: {
+ open() {
+ this.$refs.modal.show();
+ },
+ buildEvents(events) {
+ return Object.keys(events).map((key) => ({
+ id: key,
+ enabled: Boolean(events[key]),
+ name: this.$options.i18n.eventNames[key] || '',
+ loading: false,
+ }));
+ },
+ async onOpen() {
+ if (!this.events.length) {
+ await this.loadNotificationSettings();
+ }
+ },
+ async loadNotificationSettings() {
+ this.isLoading = true;
+
+ try {
+ const {
+ data: { events },
+ } = await Api.getNotificationSettings(this.projectId, this.groupId);
+
+ this.events = this.buildEvents(events);
+ } catch (error) {
+ this.$toast.show(this.$options.i18n.loadNotificationLevelErrorMessage, { type: 'error' });
+ } finally {
+ this.isLoading = false;
+ }
+ },
+ async updateEvent(isEnabled, event) {
+ const index = this.events.findIndex((e) => e.id === event.id);
+
+ // update loading state for the given event
+ this.$set(this.events, index, { ...this.events[index], loading: true });
+
+ try {
+ const {
+ data: { events },
+ } = await Api.updateNotificationSettings(this.projectId, this.groupId, {
+ [event.id]: isEnabled,
+ });
+
+ this.events = this.buildEvents(events);
+ } catch (error) {
+ this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage, { type: 'error' });
+ }
+ },
+ },
+ i18n,
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ :modal-id="modalId"
+ :title="$options.i18n.customNotificationsModal.title"
+ @show="onOpen"
+ >
+ <div class="container-fluid">
+ <div class="row">
+ <div class="col-lg-4">
+ <h4 class="gl-mt-0" data-testid="modalBodyTitle">
+ {{ $options.i18n.customNotificationsModal.bodyTitle }}
+ </h4>
+ <gl-sprintf :message="$options.i18n.customNotificationsModal.bodyMessage">
+ <template #notificationLink="{ content }">
+ <gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ <div class="col-lg-8">
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" />
+ <template v-else>
+ <gl-form-group v-for="event in events" :key="event.id">
+ <gl-form-checkbox v-model="event.enabled" @change="updateEvent($event, event)">
+ <strong>{{ event.name }}</strong
+ ><gl-loading-icon v-if="event.loading" :inline="true" class="gl-ml-2" />
+ </gl-form-checkbox>
+ </gl-form-group>
+ </template>
+ </div>
+ </div>
+ </div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/notifications/components/notifications_dropdown.vue b/app/assets/javascripts/notifications/components/notifications_dropdown.vue
new file mode 100644
index 00000000000..e4cedfdb810
--- /dev/null
+++ b/app/assets/javascripts/notifications/components/notifications_dropdown.vue
@@ -0,0 +1,196 @@
+<script>
+import {
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownDivider,
+ GlTooltipDirective,
+ GlModalDirective,
+} from '@gitlab/ui';
+import Api from '~/api';
+import { sprintf } from '~/locale';
+import { CUSTOM_LEVEL, i18n } from '../constants';
+import CustomNotificationsModal from './custom_notifications_modal.vue';
+import NotificationsDropdownItem from './notifications_dropdown_item.vue';
+
+export default {
+ name: 'NotificationsDropdown',
+ components: {
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownDivider,
+ NotificationsDropdownItem,
+ CustomNotificationsModal,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ 'gl-modal': GlModalDirective,
+ },
+ inject: {
+ containerClass: {
+ default: '',
+ },
+ disabled: {
+ default: false,
+ },
+ dropdownItems: {
+ default: [],
+ },
+ buttonSize: {
+ default: 'medium',
+ },
+ initialNotificationLevel: {
+ default: '',
+ },
+ projectId: {
+ default: null,
+ },
+ groupId: {
+ default: null,
+ },
+ showLabel: {
+ default: false,
+ },
+ },
+ data() {
+ return {
+ selectedNotificationLevel: this.initialNotificationLevel,
+ isLoading: false,
+ };
+ },
+ computed: {
+ notificationLevels() {
+ return this.dropdownItems.map((level) => ({
+ level,
+ title: this.$options.i18n.notificationTitles[level] || '',
+ description: this.$options.i18n.notificationDescriptions[level] || '',
+ }));
+ },
+ isCustomNotification() {
+ return this.selectedNotificationLevel === CUSTOM_LEVEL;
+ },
+ buttonIcon() {
+ if (this.isLoading) {
+ return null;
+ }
+
+ return this.selectedNotificationLevel === 'disabled' ? 'notifications-off' : 'notifications';
+ },
+ buttonText() {
+ return this.showLabel
+ ? this.$options.i18n.notificationTitles[this.selectedNotificationLevel]
+ : null;
+ },
+ buttonTooltip() {
+ const notificationTitle =
+ this.$options.i18n.notificationTitles[this.selectedNotificationLevel] ||
+ this.selectedNotificationLevel;
+
+ return this.disabled
+ ? this.$options.i18n.notificationDescriptions.owner_disabled
+ : sprintf(this.$options.i18n.notificationTooltipTitle, {
+ notification_title: notificationTitle,
+ });
+ },
+ },
+ methods: {
+ selectItem(level) {
+ if (level !== this.selectedNotificationLevel) {
+ this.updateNotificationLevel(level);
+ }
+ },
+ async updateNotificationLevel(level) {
+ this.isLoading = true;
+
+ try {
+ await Api.updateNotificationSettings(this.projectId, this.groupId, { level });
+ this.selectedNotificationLevel = level;
+
+ if (level === CUSTOM_LEVEL) {
+ this.$refs.customNotificationsModal.open();
+ }
+ } catch (error) {
+ this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage, { type: 'error' });
+ } finally {
+ this.isLoading = false;
+ }
+ },
+ },
+ customLevel: CUSTOM_LEVEL,
+ i18n,
+ modalId: 'custom-notifications-modal',
+};
+</script>
+
+<template>
+ <div :class="containerClass">
+ <gl-button-group
+ v-if="isCustomNotification"
+ v-gl-tooltip="{ title: buttonTooltip }"
+ data-testid="notificationButton"
+ :size="buttonSize"
+ >
+ <gl-button
+ v-gl-modal="$options.modalId"
+ :size="buttonSize"
+ :icon="buttonIcon"
+ :loading="isLoading"
+ :disabled="disabled"
+ >
+ <template v-if="buttonText">{{ buttonText }}</template>
+ </gl-button>
+ <gl-dropdown :size="buttonSize" :disabled="disabled">
+ <notifications-dropdown-item
+ v-for="item in notificationLevels"
+ :key="item.level"
+ :level="item.level"
+ :title="item.title"
+ :description="item.description"
+ :notification-level="selectedNotificationLevel"
+ @item-selected="selectItem"
+ />
+ <gl-dropdown-divider />
+ <notifications-dropdown-item
+ :key="$options.customLevel"
+ :level="$options.customLevel"
+ :title="$options.i18n.notificationTitles.custom"
+ :description="$options.i18n.notificationDescriptions.custom"
+ :notification-level="selectedNotificationLevel"
+ @item-selected="selectItem"
+ />
+ </gl-dropdown>
+ </gl-button-group>
+
+ <gl-dropdown
+ v-else
+ v-gl-tooltip="{ title: buttonTooltip }"
+ data-testid="notificationButton"
+ :text="buttonText"
+ :icon="buttonIcon"
+ :loading="isLoading"
+ :size="buttonSize"
+ :disabled="disabled"
+ >
+ <notifications-dropdown-item
+ v-for="item in notificationLevels"
+ :key="item.level"
+ :level="item.level"
+ :title="item.title"
+ :description="item.description"
+ :notification-level="selectedNotificationLevel"
+ @item-selected="selectItem"
+ />
+ <gl-dropdown-divider />
+ <notifications-dropdown-item
+ :key="$options.customLevel"
+ :level="$options.customLevel"
+ :title="$options.i18n.notificationTitles.custom"
+ :description="$options.i18n.notificationDescriptions.custom"
+ :notification-level="selectedNotificationLevel"
+ @item-selected="selectItem"
+ />
+ </gl-dropdown>
+ <custom-notifications-modal ref="customNotificationsModal" :modal-id="$options.modalId" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/notifications/components/notifications_dropdown_item.vue b/app/assets/javascripts/notifications/components/notifications_dropdown_item.vue
new file mode 100644
index 00000000000..73bb9c1b36f
--- /dev/null
+++ b/app/assets/javascripts/notifications/components/notifications_dropdown_item.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ name: 'NotificationsDropdownItem',
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ level: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ description: {
+ type: String,
+ required: true,
+ },
+ notificationLevel: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isActive() {
+ return this.notificationLevel === this.level;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-item is-check-item :is-checked="isActive" @click="$emit('item-selected', level)">
+ <div class="gl-display-flex gl-flex-direction-column">
+ <span class="gl-font-weight-bold">{{ title }}</span>
+ <span class="gl-text-gray-500">{{ description }}</span>
+ </div>
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/notifications/constants.js b/app/assets/javascripts/notifications/constants.js
new file mode 100644
index 00000000000..07c569a0293
--- /dev/null
+++ b/app/assets/javascripts/notifications/constants.js
@@ -0,0 +1,58 @@
+import { __, s__ } from '~/locale';
+
+export const CUSTOM_LEVEL = 'custom';
+
+export const i18n = {
+ notificationTitles: {
+ participating: s__('NotificationLevel|Participate'),
+ mention: s__('NotificationLevel|On mention'),
+ watch: s__('NotificationLevel|Watch'),
+ global: s__('NotificationLevel|Global'),
+ disabled: s__('NotificationLevel|Disabled'),
+ custom: s__('NotificationLevel|Custom'),
+ },
+ notificationTooltipTitle: __('Notification setting - %{notification_title}'),
+ notificationDescriptions: {
+ participating: __('You will only receive notifications for threads you have participated in'),
+ mention: __('You will receive notifications only for comments in which you were @mentioned'),
+ watch: __('You will receive notifications for any activity'),
+ disabled: __('You will not get any notifications via email'),
+ global: __('Use your global notification setting'),
+ custom: __('You will only receive notifications for the events you choose'),
+ owner_disabled: __('Notifications have been disabled by the project or group owner'),
+ },
+ updateNotificationLevelErrorMessage: __(
+ 'An error occured while updating the notification settings. Please try again.',
+ ),
+ loadNotificationLevelErrorMessage: __(
+ 'An error occured while loading the notification settings. Please try again.',
+ ),
+ customNotificationsModal: {
+ title: __('Custom notification events'),
+ bodyTitle: __('Notification events'),
+ bodyMessage: __(
+ 'Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notificationLinkStart} notification emails%{notificationLinkEnd}.',
+ ),
+ },
+ eventNames: {
+ change_reviewer_merge_request: s__('NotificationEvent|Change reviewer merge request'),
+ close_issue: s__('NotificationEvent|Close issue'),
+ close_merge_request: s__('NotificationEvent|Close merge request'),
+ failed_pipeline: s__('NotificationEvent|Failed pipeline'),
+ fixed_pipeline: s__('NotificationEvent|Fixed pipeline'),
+ issue_due: s__('NotificationEvent|Issue due'),
+ merge_merge_request: s__('NotificationEvent|Merge merge request'),
+ moved_project: s__('NotificationEvent|Moved project'),
+ new_epic: s__('NotificationEvent|New epic'),
+ new_issue: s__('NotificationEvent|New issue'),
+ new_merge_request: s__('NotificationEvent|New merge request'),
+ new_note: s__('NotificationEvent|New note'),
+ new_release: s__('NotificationEvent|New release'),
+ push_to_merge_request: s__('NotificationEvent|Push to merge request'),
+ reassign_issue: s__('NotificationEvent|Reassign issue'),
+ reassign_merge_request: s__('NotificationEvent|Reassign merge request'),
+ reopen_issue: s__('NotificationEvent|Reopen issue'),
+ reopen_merge_request: s__('NotificationEvent|Reopen merge request'),
+ success_pipeline: s__('NotificationEvent|Successful pipeline'),
+ },
+};
diff --git a/app/assets/javascripts/notifications/index.js b/app/assets/javascripts/notifications/index.js
new file mode 100644
index 00000000000..d60a368703c
--- /dev/null
+++ b/app/assets/javascripts/notifications/index.js
@@ -0,0 +1,44 @@
+import { GlToast } from '@gitlab/ui';
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import NotificationsDropdown from './components/notifications_dropdown.vue';
+
+Vue.use(GlToast);
+
+export default () => {
+ const containers = document.querySelectorAll('.js-vue-notification-dropdown');
+
+ if (!containers.length) return false;
+
+ return containers.forEach((el) => {
+ const {
+ containerClass,
+ buttonSize,
+ disabled,
+ dropdownItems,
+ notificationLevel,
+ helpPagePath,
+ projectId,
+ groupId,
+ showLabel,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ containerClass,
+ buttonSize,
+ disabled: parseBoolean(disabled),
+ dropdownItems: JSON.parse(dropdownItems),
+ initialNotificationLevel: notificationLevel,
+ helpPagePath,
+ projectId,
+ groupId,
+ showLabel: parseBoolean(showLabel),
+ },
+ render(h) {
+ return h(NotificationsDropdown);
+ },
+ });
+ });
+};
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index eaa5ec3a2e4..d61defed14d 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { Rails } from '~/lib/utils/rails_ujs';
-import { deprecatedCreateFlash as Flash } from './flash';
import { __ } from '~/locale';
+import { deprecatedCreateFlash as Flash } from './flash';
export default function notificationsDropdown() {
$(document).on('click', '.update-notification', function updateNotificationCallback(e) {
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 1b12fece23a..8b90da71bef 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import { __ } from './locale';
-import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from './flash';
+import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
export default class NotificationsForm {
constructor() {
diff --git a/app/assets/javascripts/onboarding_issues/index.js b/app/assets/javascripts/onboarding_issues/index.js
deleted file mode 100644
index b23a10c9254..00000000000
--- a/app/assets/javascripts/onboarding_issues/index.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import $ from 'jquery';
-import { parseBoolean, getCookie, setCookie, removeCookie } from '~/lib/utils/common_utils';
-import { __, sprintf } from '~/locale';
-import Tracking from '~/tracking';
-
-const COOKIE_NAME = 'onboarding_issues_settings';
-
-const POPOVER_LOCATIONS = {
- GROUPS_SHOW: 'groups#show',
- PROJECTS_SHOW: 'projects#show',
- ISSUES_INDEX: 'issues#index',
-};
-
-const removeLearnGitLabCookie = () => {
- removeCookie(COOKIE_NAME);
-};
-
-function disposePopover(event) {
- event.preventDefault();
- this.popover('dispose');
- removeLearnGitLabCookie();
- Tracking.event('Growth::Conversion::Experiment::OnboardingIssues', 'dismiss_popover');
-}
-
-const showPopover = (el, path, footer, options) => {
- // Cookie value looks like `{ 'groups#show': true, 'projects#show': true, 'issues#index': true }`. When it doesn't exist, don't show the popover.
- const cookie = getCookie(COOKIE_NAME);
- if (!cookie) return;
-
- // When the popover action has already been taken, don't show the popover.
- const settings = JSON.parse(cookie);
- if (!parseBoolean(settings[path])) return;
-
- const defaultOptions = {
- boundary: 'window',
- html: true,
- placement: 'top',
- template: `<div class="popover blue learn-gitlab d-none d-xl-block" role="tooltip">
- <div class="arrow"></div>
- <div class="close cursor-pointer gl-font-base text-white gl-opacity-10 p-2">&#10005</div>
- <div class="popover-body gl-font-base gl-line-height-20 pb-0 px-3"></div>
- <div class="bold text-right text-white p-2">${footer}</div>
- </div>`,
- };
-
- // When one of the popovers is dismissed, remove the cookie.
- const closeButton = () => document.querySelector('.learn-gitlab.popover .close');
-
- // We still have to use jQuery, since Bootstrap's Popover is based on jQuery.
- const jQueryEl = $(el);
- const clickCloseButton = disposePopover.bind(jQueryEl);
-
- jQueryEl
- .popover({ ...defaultOptions, ...options })
- .on('inserted.bs.popover', () => closeButton().addEventListener('click', clickCloseButton))
- .on('hide.bs.dropdown', () => closeButton().removeEventListener('click', clickCloseButton))
- .popover('show');
-
- // The previous popover actions have been taken, don't show those popovers anymore.
- Object.keys(settings).forEach((pathSetting) => {
- if (path !== pathSetting) {
- settings[pathSetting] = false;
- } else {
- setCookie(COOKIE_NAME, settings);
- }
- });
-
- // The final popover action will be taken on click, we then no longer need the cookie.
- if (path === POPOVER_LOCATIONS.ISSUES_INDEX) {
- el.addEventListener('click', removeLearnGitLabCookie);
- }
-};
-
-export const showLearnGitLabGroupItemPopover = (id) => {
- const el = document.querySelector(`#group-${id} .group-text a`);
-
- if (!el) return;
-
- const options = {
- content: __(
- 'Here are all your projects in your group, including the one you just created. To start, let’s take a look at your personalized learning project which will help you learn about GitLab at your own pace.',
- ),
- };
-
- showPopover(el, POPOVER_LOCATIONS.GROUPS_SHOW, '1 / 2', options);
-};
-
-export const showLearnGitLabProjectPopover = () => {
- // Do not show a popover if we are not viewing the 'Learn GitLab' project.
- if (!window.location.pathname.includes('learn-gitlab')) return;
-
- const el = document.querySelector('a.shortcuts-issues');
-
- if (!el) return;
-
- const options = {
- content: sprintf(
- __(
- 'Go to %{strongStart}Issues%{strongEnd} &gt; %{strongStart}Boards%{strongEnd} to access your personalized learning issue board.',
- ),
- { strongStart: '<strong>', strongEnd: '</strong>' },
- false,
- ),
- };
-
- showPopover(el, POPOVER_LOCATIONS.PROJECTS_SHOW, '2 / 2', options);
-};
-
-export const showLearnGitLabIssuesPopover = () => {
- // Do not show a popover if we are not viewing the 'Learn GitLab' project.
- if (!window.location.pathname.includes('learn-gitlab')) return;
-
- const el = document.querySelector('a[data-qa-selector="issue_boards_link"]');
-
- if (!el) return;
-
- const options = {
- content: sprintf(
- __(
- 'Go to %{strongStart}Issues%{strongEnd} &gt; %{strongStart}Boards%{strongEnd} to access your personalized learning issue board.',
- ),
- { strongStart: '<strong>', strongEnd: '</strong>' },
- false,
- ),
- };
-
- showPopover(el, POPOVER_LOCATIONS.ISSUES_INDEX, '2 / 2', options);
-};
diff --git a/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue b/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue
index d83146d2f5e..7120ad511d3 100644
--- a/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue
+++ b/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
import { s__ } from '~/locale';
import { timezones } from '~/monitoring/format_date';
diff --git a/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue
index 812c5a3fe9a..2ea5b4e01b1 100644
--- a/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue
+++ b/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/operation_settings/components/metrics_settings.vue b/app/assets/javascripts/operation_settings/components/metrics_settings.vue
index 2e972dd7154..0c0bbb744b3 100644
--- a/app/assets/javascripts/operation_settings/components/metrics_settings.vue
+++ b/app/assets/javascripts/operation_settings/components/metrics_settings.vue
@@ -1,8 +1,8 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlButton, GlLink } from '@gitlab/ui';
-import ExternalDashboard from './form_group/external_dashboard.vue';
+import { mapState, mapActions } from 'vuex';
import DashboardTimezone from './form_group/dashboard_timezone.vue';
+import ExternalDashboard from './form_group/external_dashboard.vue';
export default {
components: {
@@ -31,9 +31,9 @@ export default {
<template>
<section class="settings no-animate">
<div class="settings-header">
- <h3 class="js-section-header h4">
+ <h4 class="js-section-header">
{{ s__('MetricsSettings|Metrics dashboard') }}
- </h3>
+ </h4>
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{ s__('MetricsSettings|Manage Metrics Dashboard settings.') }}
diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js
index 426a060949e..faddf9c7b81 100644
--- a/app/assets/javascripts/operation_settings/index.js
+++ b/app/assets/javascripts/operation_settings/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from './store';
import MetricsSettingsForm from './components/metrics_settings.vue';
+import store from './store';
export default () => {
const el = document.querySelector('.js-operation-settings');
diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js
index bbbff6630d3..af66e344b35 100644
--- a/app/assets/javascripts/operation_settings/store/actions.js
+++ b/app/assets/javascripts/operation_settings/store/actions.js
@@ -1,7 +1,7 @@
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import * as mutationTypes from './mutation_types';
export const setExternalDashboardUrl = ({ commit }, url) =>
diff --git a/app/assets/javascripts/operation_settings/store/index.js b/app/assets/javascripts/operation_settings/store/index.js
index e3dcfd31a83..a11bd8089fd 100644
--- a/app/assets/javascripts/operation_settings/store/index.js
+++ b/app/assets/javascripts/operation_settings/store/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
+import createState from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue
index c9f1c8b903c..bcfb17a1529 100644
--- a/app/assets/javascripts/packages/details/components/app.vue
+++ b/app/assets/javascripts/packages/details/components/app.vue
@@ -11,20 +11,20 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import Tracking from '~/tracking';
-import { s__ } from '~/locale';
import { objectToQueryString } from '~/lib/utils/common_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import PackageHistory from './package_history.vue';
-import PackageTitle from './package_title.vue';
-import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
+import { s__ } from '~/locale';
+import Tracking from '~/tracking';
import PackageListRow from '../../shared/components/package_list_row.vue';
-import { packageTypeToTrackCategory } from '../../shared/utils';
+import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
import { PackageType, TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '../../shared/constants';
-import DependencyRow from './dependency_row.vue';
+import { packageTypeToTrackCategory } from '../../shared/utils';
import AdditionalMetadata from './additional_metadata.vue';
+import DependencyRow from './dependency_row.vue';
import InstallationCommands from './installation_commands.vue';
import PackageFiles from './package_files.vue';
+import PackageHistory from './package_history.vue';
+import PackageTitle from './package_title.vue';
export default {
name: 'PackagesApp',
diff --git a/app/assets/javascripts/packages/details/components/installation_commands.vue b/app/assets/javascripts/packages/details/components/installation_commands.vue
index 138103020a7..28978913e6e 100644
--- a/app/assets/javascripts/packages/details/components/installation_commands.vue
+++ b/app/assets/javascripts/packages/details/components/installation_commands.vue
@@ -1,11 +1,11 @@
<script>
+import { PackageType } from '../../shared/constants';
+import ComposerInstallation from './composer_installation.vue';
import ConanInstallation from './conan_installation.vue';
import MavenInstallation from './maven_installation.vue';
import NpmInstallation from './npm_installation.vue';
import NugetInstallation from './nuget_installation.vue';
import PypiInstallation from './pypi_installation.vue';
-import ComposerInstallation from './composer_installation.vue';
-import { PackageType } from '../../shared/constants';
export default {
name: 'InstallationCommands',
diff --git a/app/assets/javascripts/packages/details/components/package_files.vue b/app/assets/javascripts/packages/details/components/package_files.vue
index 3add454fda3..c5e929fe2a4 100644
--- a/app/assets/javascripts/packages/details/components/package_files.vue
+++ b/app/assets/javascripts/packages/details/components/package_files.vue
@@ -1,11 +1,11 @@
<script>
import { GlLink, GlTable } from '@gitlab/ui';
import { last } from 'lodash';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import Tracking from '~/tracking';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
name: 'PackageFiles',
diff --git a/app/assets/javascripts/packages/details/components/package_history.vue b/app/assets/javascripts/packages/details/components/package_history.vue
index 62550602428..0d7a73c12f1 100644
--- a/app/assets/javascripts/packages/details/components/package_history.vue
+++ b/app/assets/javascripts/packages/details/components/package_history.vue
@@ -1,11 +1,11 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { first } from 'lodash';
-import { s__, n__ } from '~/locale';
import { truncateSha } from '~/lib/utils/text_utility';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
+import { s__, n__ } from '~/locale';
import { HISTORY_PIPELINES_LIMIT } from '~/packages/details/constants';
+import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
name: 'PackageHistory',
diff --git a/app/assets/javascripts/packages/details/components/package_title.vue b/app/assets/javascripts/packages/details/components/package_title.vue
index 6b7eeacb964..d02a7b3ec27 100644
--- a/app/assets/javascripts/packages/details/components/package_title.vue
+++ b/app/assets/javascripts/packages/details/components/package_title.vue
@@ -1,14 +1,14 @@
<script>
/* eslint-disable vue/v-slot-style */
-import { mapState, mapGetters } from 'vuex';
import { GlIcon, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
-import PackageTags from '../../shared/components/package_tags.vue';
+import { mapState, mapGetters } from 'vuex';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import { __ } from '~/locale';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import PackageTags from '../../shared/components/package_tags.vue';
export default {
name: 'PackageTitle',
diff --git a/app/assets/javascripts/packages/details/index.js b/app/assets/javascripts/packages/details/index.js
index 233da3e4a99..5b9d58a3860 100644
--- a/app/assets/javascripts/packages/details/index.js
+++ b/app/assets/javascripts/packages/details/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import PackagesApp from './components/app.vue';
import Translate from '~/vue_shared/translate';
+import PackagesApp from './components/app.vue';
import createStore from './store';
Vue.use(Translate);
diff --git a/app/assets/javascripts/packages/details/store/actions.js b/app/assets/javascripts/packages/details/store/actions.js
index 340f60258a0..87216366c8b 100644
--- a/app/assets/javascripts/packages/details/store/actions.js
+++ b/app/assets/javascripts/packages/details/store/actions.js
@@ -1,7 +1,7 @@
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
+import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
import * as types from './mutation_types';
export const fetchPackageVersions = ({ commit, state }) => {
diff --git a/app/assets/javascripts/packages/list/components/package_search.vue b/app/assets/javascripts/packages/list/components/package_search.vue
new file mode 100644
index 00000000000..cd61d323d83
--- /dev/null
+++ b/app/assets/javascripts/packages/list/components/package_search.vue
@@ -0,0 +1,50 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { __, s__ } from '~/locale';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import getTableHeaders from '../utils';
+import PackageTypeToken from './tokens/package_type_token.vue';
+
+export default {
+ tokens: [
+ {
+ type: 'type',
+ icon: 'package',
+ title: s__('PackageRegistry|Type'),
+ unique: true,
+ token: PackageTypeToken,
+ operators: [{ value: '=', description: __('is'), default: 'true' }],
+ },
+ ],
+ components: { RegistrySearch },
+ computed: {
+ ...mapState({
+ isGroupPage: (state) => state.config.isGroupPage,
+ sorting: (state) => state.sorting,
+ filter: (state) => state.filter,
+ }),
+ sortableFields() {
+ return getTableHeaders(this.isGroupPage);
+ },
+ },
+ methods: {
+ ...mapActions(['setSorting', 'setFilter']),
+ updateSorting(newValue) {
+ this.setSorting(newValue);
+ this.$emit('update');
+ },
+ },
+};
+</script>
+
+<template>
+ <registry-search
+ :filter="filter"
+ :sorting="sorting"
+ :tokens="$options.tokens"
+ :sortable-fields="sortableFields"
+ @sorting:changed="updateSorting"
+ @filter:changed="setFilter"
+ @filter:submit="$emit('update')"
+ />
+</template>
diff --git a/app/assets/javascripts/packages/list/components/package_title.vue b/app/assets/javascripts/packages/list/components/package_title.vue
index f94a98e4ca7..6176e15ffd4 100644
--- a/app/assets/javascripts/packages/list/components/package_title.vue
+++ b/app/assets/javascripts/packages/list/components/package_title.vue
@@ -1,7 +1,7 @@
<script>
import { n__ } from '~/locale';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '../constants';
export default {
diff --git a/app/assets/javascripts/packages/list/components/packages_filter.vue b/app/assets/javascripts/packages/list/components/packages_filter.vue
deleted file mode 100644
index 17398071217..00000000000
--- a/app/assets/javascripts/packages/list/components/packages_filter.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-<script>
-import { GlSearchBoxByClick } from '@gitlab/ui';
-import { mapActions } from 'vuex';
-
-export default {
- components: {
- GlSearchBoxByClick,
- },
- methods: {
- ...mapActions(['setFilter']),
- },
-};
-</script>
-
-<template>
- <gl-search-box-by-click
- :placeholder="s__('PackageRegistry|Filter by name')"
- @submit="$emit('filter')"
- @input="setFilter"
- />
-</template>
diff --git a/app/assets/javascripts/packages/list/components/packages_list.vue b/app/assets/javascripts/packages/list/components/packages_list.vue
index 1e38ee525b8..23ba070aa26 100644
--- a/app/assets/javascripts/packages/list/components/packages_list.vue
+++ b/app/assets/javascripts/packages/list/components/packages_list.vue
@@ -1,12 +1,12 @@
<script>
-import { mapState, mapGetters } from 'vuex';
import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
-import Tracking from '~/tracking';
+import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import PackagesListRow from '../../shared/components/package_list_row.vue';
+import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
import { TrackingActions } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils';
-import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
-import PackagesListRow from '../../shared/components/package_list_row.vue';
export default {
components: {
diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue
index 2a786b92515..a609dfebedf 100644
--- a/app/assets/javascripts/packages/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue
@@ -1,39 +1,43 @@
<script>
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
import createFlash from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
-import PackageFilter from './packages_filter.vue';
-import PackageList from './packages_list.vue';
-import PackageSort from './packages_sort.vue';
-import { PACKAGE_REGISTRY_TABS, DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
+import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
+import PackageSearch from './package_search.vue';
import PackageTitle from './package_title.vue';
+import PackageList from './packages_list.vue';
export default {
components: {
GlEmptyState,
- GlTab,
- GlTabs,
GlLink,
GlSprintf,
- PackageFilter,
PackageList,
- PackageSort,
PackageTitle,
+ PackageSearch,
},
computed: {
...mapState({
emptyListIllustration: (state) => state.config.emptyListIllustration,
emptyListHelpUrl: (state) => state.config.emptyListHelpUrl,
- filterQuery: (state) => state.filterQuery,
+ filter: (state) => state.filter,
selectedType: (state) => state.selectedType,
packageHelpUrl: (state) => state.config.packageHelpUrl,
packagesCount: (state) => state.pagination?.total,
}),
- tabsToRender() {
- return PACKAGE_REGISTRY_TABS;
+ emptySearch() {
+ return (
+ this.filter.filter((f) => f.type !== 'filtered-search-term' || f.value?.data).length === 0
+ );
+ },
+
+ emptyStateTitle() {
+ return this.emptySearch
+ ? s__('PackageRegistry|There are no packages yet')
+ : s__('PackageRegistry|Sorry, your filter produced no results');
},
},
mounted() {
@@ -48,27 +52,6 @@ export default {
onPackageDeleteRequest(item) {
return this.requestDeletePackage(item);
},
- tabChanged(index) {
- const selected = PACKAGE_REGISTRY_TABS[index];
-
- if (selected !== this.selectedType) {
- this.setSelectedType(selected);
- this.requestPackagesList();
- }
- },
- emptyStateTitle({ title, type }) {
- if (this.filterQuery) {
- return s__('PackageRegistry|Sorry, your filter produced no results');
- }
-
- if (type) {
- return sprintf(s__('PackageRegistry|There are no %{packageType} packages yet'), {
- packageType: title,
- });
- }
-
- return s__('PackageRegistry|There are no packages yet');
- },
checkDeleteAlert() {
const urlParams = new URLSearchParams(window.location.search);
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
@@ -92,33 +75,21 @@ export default {
<template>
<div>
<package-title :package-help-url="packageHelpUrl" :packages-count="packagesCount" />
+ <package-search @update="requestPackagesList" />
- <gl-tabs @input="tabChanged">
- <template #tabs-end>
- <div
- class="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end"
- >
- <package-filter class="gl-mr-2" @filter="requestPackagesList" />
- <package-sort @sort:changed="requestPackagesList" />
- </div>
- </template>
-
- <gl-tab v-for="(tab, index) in tabsToRender" :key="index" :title="tab.title">
- <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
- <template #empty-state>
- <gl-empty-state :title="emptyStateTitle(tab)" :svg-path="emptyListIllustration">
- <template #description>
- <gl-sprintf v-if="filterQuery" :message="$options.i18n.widenFilters" />
- <gl-sprintf v-else :message="$options.i18n.noResults">
- <template #noPackagesLink="{ content }">
- <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
+ <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
+ <template #empty-state>
+ <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
+ <template #description>
+ <gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" />
+ <gl-sprintf v-else :message="$options.i18n.noResults">
+ <template #noPackagesLink="{ content }">
+ <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
</template>
- </gl-empty-state>
+ </gl-sprintf>
</template>
- </package-list>
- </gl-tab>
- </gl-tabs>
+ </gl-empty-state>
+ </template>
+ </package-list>
</div>
</template>
diff --git a/app/assets/javascripts/packages/list/components/packages_sort.vue b/app/assets/javascripts/packages/list/components/packages_sort.vue
deleted file mode 100644
index 4b2d9091f8f..00000000000
--- a/app/assets/javascripts/packages/list/components/packages_sort.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<script>
-import { GlSorting, GlSortingItem } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
-import { ASCENDING_ODER, DESCENDING_ORDER } from '../constants';
-import getTableHeaders from '../utils';
-
-export default {
- name: 'PackageSort',
- components: {
- GlSorting,
- GlSortingItem,
- },
- computed: {
- ...mapState({
- isGroupPage: (state) => state.config.isGroupPage,
- orderBy: (state) => state.sorting.orderBy,
- sort: (state) => state.sorting.sort,
- }),
- sortText() {
- const field = this.sortableFields.find((s) => s.orderBy === this.orderBy);
- return field ? field.label : '';
- },
- sortableFields() {
- return getTableHeaders(this.isGroupPage);
- },
- isSortAscending() {
- return this.sort === ASCENDING_ODER;
- },
- },
- methods: {
- ...mapActions(['setSorting']),
- onDirectionChange() {
- const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
- this.setSorting({ sort });
- this.$emit('sort:changed');
- },
- onSortItemClick(item) {
- this.setSorting({ orderBy: item });
- this.$emit('sort:changed');
- },
- },
-};
-</script>
-
-<template>
- <gl-sorting
- :text="sortText"
- :is-ascending="isSortAscending"
- @sortDirectionChange="onDirectionChange"
- >
- <gl-sorting-item
- v-for="item in sortableFields"
- ref="packageListSortItem"
- :key="item.orderBy"
- @click="onSortItemClick(item.orderBy)"
- >
- {{ item.label }}
- </gl-sorting-item>
- </gl-sorting>
-</template>
diff --git a/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue b/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue
new file mode 100644
index 00000000000..74b6774712e
--- /dev/null
+++ b/app/assets/javascripts/packages/list/components/tokens/package_type_token.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { PACKAGE_TYPES } from '../../constants';
+
+export default {
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ },
+ PACKAGE_TYPES,
+};
+</script>
+
+<template>
+ <gl-filtered-search-token v-bind="{ ...$attrs }" v-on="$listeners">
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="(type, index) in $options.PACKAGE_TYPES"
+ :key="index"
+ :value="type.type"
+ >
+ {{ type.title }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
index e14696e0d1c..d47eb8c3421 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -26,9 +26,6 @@ export const LIST_LABEL_PACKAGE_TYPE = __('Type');
export const LIST_LABEL_CREATED_AT = __('Published');
export const LIST_LABEL_ACTIONS = '';
-export const ASCENDING_ODER = 'asc';
-export const DESCENDING_ORDER = 'desc';
-
// The following is not translated because it is used to build a JavaScript exception error message
export const MISSING_DELETE_PATH_ERROR = 'Missing delete_api_path link';
@@ -55,11 +52,7 @@ export const SORT_FIELDS = [
},
];
-export const PACKAGE_REGISTRY_TABS = [
- {
- title: __('All'),
- type: null,
- },
+export const PACKAGE_TYPES = [
{
title: s__('PackageRegistry|Composer'),
type: PackageType.COMPOSER,
diff --git a/app/assets/javascripts/packages/list/packages_list_app_bundle.js b/app/assets/javascripts/packages/list/packages_list_app_bundle.js
index 2f240cff143..58b09c1ebd1 100644
--- a/app/assets/javascripts/packages/list/packages_list_app_bundle.js
+++ b/app/assets/javascripts/packages/list/packages_list_app_bundle.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import Translate from '~/vue_shared/translate';
-import { createStore } from './stores';
import PackagesListApp from './components/packages_list_app.vue';
-import createDefaultClient from '~/lib/graphql';
+import { createStore } from './stores';
Vue.use(VueApollo);
Vue.use(Translate);
diff --git a/app/assets/javascripts/packages/list/stores/actions.js b/app/assets/javascripts/packages/list/stores/actions.js
index bbc11e3cf13..195117b9ddb 100644
--- a/app/assets/javascripts/packages/list/stores/actions.js
+++ b/app/assets/javascripts/packages/list/stores/actions.js
@@ -1,8 +1,7 @@
import Api from '~/api';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
-import * as types from './mutation_types';
import {
FETCH_PACKAGES_LIST_ERROR_MESSAGE,
DELETE_PACKAGE_SUCCESS_MESSAGE,
@@ -11,11 +10,11 @@ import {
MISSING_DELETE_PATH_ERROR,
} from '../constants';
import { getNewPaginationPage } from '../utils';
+import * as types from './mutation_types';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data);
export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);
-export const setSelectedType = ({ commit }, data) => commit(types.SET_SELECTED_TYPE, data);
export const setFilter = ({ commit }, data) => commit(types.SET_FILTER, data);
export const receivePackagesListSuccess = ({ commit }, { data, headers }) => {
@@ -29,9 +28,9 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params;
const { sort, orderBy } = state.sorting;
- const type = state.selectedType?.type?.toLowerCase();
- const nameFilter = state.filterQuery?.toLowerCase();
- const packageFilters = { package_type: type, package_name: nameFilter };
+ const type = state.filter.find((f) => f.type === 'type');
+ const name = state.filter.find((f) => f.type === 'filtered-search-term');
+ const packageFilters = { package_type: type?.value?.data, package_name: name?.value?.data };
const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages';
diff --git a/app/assets/javascripts/packages/list/stores/getters.js b/app/assets/javascripts/packages/list/stores/getters.js
index 85c489deda0..482c111b58b 100644
--- a/app/assets/javascripts/packages/list/stores/getters.js
+++ b/app/assets/javascripts/packages/list/stores/getters.js
@@ -1,5 +1,5 @@
-import { LIST_KEY_PROJECT } from '../constants';
import { beautifyPath } from '../../shared/utils';
+import { LIST_KEY_PROJECT } from '../constants';
export default (state) =>
state.packages.map((p) => ({ ...p, projectPathName: beautifyPath(p[LIST_KEY_PROJECT]) }));
diff --git a/app/assets/javascripts/packages/list/stores/mutation_types.js b/app/assets/javascripts/packages/list/stores/mutation_types.js
index a5a584ccf1f..561ad97f7e3 100644
--- a/app/assets/javascripts/packages/list/stores/mutation_types.js
+++ b/app/assets/javascripts/packages/list/stores/mutation_types.js
@@ -4,5 +4,4 @@ export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS';
export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_SORTING = 'SET_SORTING';
-export const SET_SELECTED_TYPE = 'SET_SELECTED_TYPE';
export const SET_FILTER = 'SET_FILTER';
diff --git a/app/assets/javascripts/packages/list/stores/mutations.js b/app/assets/javascripts/packages/list/stores/mutations.js
index 2fe7981b3d9..4ce13cfcb29 100644
--- a/app/assets/javascripts/packages/list/stores/mutations.js
+++ b/app/assets/javascripts/packages/list/stores/mutations.js
@@ -1,6 +1,6 @@
-import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { GROUP_PAGE_TYPE } from '../constants';
+import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, config) {
@@ -28,11 +28,7 @@ export default {
state.sorting = { ...state.sorting, ...sorting };
},
- [types.SET_SELECTED_TYPE](state, type) {
- state.selectedType = type;
- },
-
- [types.SET_FILTER](state, query) {
- state.filterQuery = query;
+ [types.SET_FILTER](state, filter) {
+ state.filter = filter;
},
};
diff --git a/app/assets/javascripts/packages/list/stores/state.js b/app/assets/javascripts/packages/list/stores/state.js
index 18ab2390b87..60f02eddc9f 100644
--- a/app/assets/javascripts/packages/list/stores/state.js
+++ b/app/assets/javascripts/packages/list/stores/state.js
@@ -1,5 +1,3 @@
-import { PACKAGE_REGISTRY_TABS } from '../constants';
-
export default () => ({
/**
* Determine if the component is loading data from the API
@@ -49,9 +47,8 @@ export default () => ({
/**
* The search query that is used to filter packages by name
*/
- filterQuery: '',
+ filter: [],
/**
* The selected TAB of the package types tabs
*/
- selectedType: PACKAGE_REGISTRY_TABS[0],
});
diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages/shared/components/package_list_row.vue
index d55ca80a7fc..cdfe042c39f 100644
--- a/app/assets/javascripts/packages/shared/components/package_list_row.vue
+++ b/app/assets/javascripts/packages/shared/components/package_list_row.vue
@@ -1,11 +1,11 @@
<script>
import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
-import PackageTags from './package_tags.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { getPackageTypeLabel } from '../utils';
import PackagePath from './package_path.vue';
+import PackageTags from './package_tags.vue';
import PublishMethod from './publish_method.vue';
-import { getPackageTypeLabel } from '../utils';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-import ListItem from '~/vue_shared/components/registry/list_item.vue';
export default {
name: 'PackageListRow',
@@ -88,7 +88,7 @@ export default {
<div class="gl-display-flex">
<span>{{ packageEntity.version }}</span>
- <div v-if="hasPipeline" class="gl-display-none gl-display-sm-flex gl-ml-2">
+ <div v-if="hasPipeline" class="gl-display-none gl-sm-display-flex gl-ml-2">
<gl-sprintf :message="s__('PackageRegistry|published by %{author}')">
<template #author>{{ packageEntity.pipeline.user.name }}</template>
</gl-sprintf>
diff --git a/app/assets/javascripts/packages/shared/components/package_tags.vue b/app/assets/javascripts/packages/shared/components/package_tags.vue
index 5172b855fc3..5ec950e4d45 100644
--- a/app/assets/javascripts/packages/shared/components/package_tags.vue
+++ b/app/assets/javascripts/packages/shared/components/package_tags.vue
@@ -91,7 +91,7 @@ export default {
variant="muted"
:title="moreTagsTooltip"
size="sm"
- class="gl-display-none gl-display-md-flex gl-ml-2"
+ class="gl-display-none gl-md-display-flex gl-ml-2"
><gl-sprintf :message="__('+%{tags} more')">
<template #tags>
{{ moreTagsDisplay }}
@@ -103,7 +103,7 @@ export default {
v-if="moreTagsDisplay && hideLabel"
data-testid="moreBadge"
variant="muted"
- class="gl-display-md-none gl-ml-2"
+ class="gl-md-display-none gl-ml-2"
>{{ tagsDisplay }}</gl-badge
>
</div>
diff --git a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
index efd9f8db908..cf555f46f8c 100644
--- a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
+++ b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
@@ -21,7 +21,7 @@ export default {
<template>
<div>
- <div class="gl-flex-direction-column gl-display-sm-none" data-testid="mobile-loader">
+ <div class="gl-flex-direction-column gl-sm-display-none" data-testid="mobile-loader">
<gl-skeleton-loader
v-for="index in $options.rowsToRender.mobile"
:key="index"
@@ -37,7 +37,7 @@ export default {
</gl-skeleton-loader>
</div>
<div
- class="gl-display-none gl-display-sm-flex gl-flex-direction-column"
+ class="gl-display-none gl-sm-display-flex gl-flex-direction-column"
data-testid="desktop-loader"
>
<gl-skeleton-loader
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
index a3d507180c6..5cd8261ac23 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js
@@ -1,9 +1,13 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
+
+import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '~/vue_shared/translate';
import SettingsApp from './components/group_settings_app.vue';
import { apolloProvider } from './graphql';
Vue.use(Translate);
+Vue.use(GlToast);
export default () => {
const el = document.getElementById('js-packages-and-registries-settings');
@@ -13,6 +17,10 @@ export default () => {
return new Vue({
el,
apolloProvider,
+ provide: {
+ defaultExpanded: parseBoolean(el.dataset.defaultExpanded),
+ groupPath: el.dataset.groupPath,
+ },
render(createElement) {
return createElement(SettingsApp);
},
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
index 6bcecf43a13..933cbeaedce 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
@@ -1,9 +1,138 @@
<script>
+import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
+import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
+
+import {
+ PACKAGE_SETTINGS_HEADER,
+ PACKAGE_SETTINGS_DESCRIPTION,
+ PACKAGES_DOCS_PATH,
+ ERROR_UPDATING_SETTINGS,
+ SUCCESS_UPDATING_SETTINGS,
+} from '~/packages_and_registries/settings/group/constants';
+import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
+import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
+import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update';
+import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+
export default {
name: 'GroupSettingsApp',
+ i18n: {
+ PACKAGE_SETTINGS_HEADER,
+ PACKAGE_SETTINGS_DESCRIPTION,
+ },
+ links: {
+ PACKAGES_DOCS_PATH,
+ },
+ components: {
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ SettingsBlock,
+ MavenSettings,
+ },
+ inject: ['defaultExpanded', 'groupPath'],
+ apollo: {
+ packageSettings: {
+ query: getGroupPackagesSettingsQuery,
+ variables() {
+ return {
+ fullPath: this.groupPath,
+ };
+ },
+ update(data) {
+ return data.group?.packageSettings;
+ },
+ },
+ },
+ data() {
+ return {
+ packageSettings: {},
+ errors: {},
+ alertMessage: null,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.packageSettings.loading;
+ },
+ },
+ methods: {
+ dismissAlert() {
+ this.alertMessage = null;
+ },
+ updateSettings(payload) {
+ this.errors = {};
+ return this.$apollo
+ .mutate({
+ mutation: updateNamespacePackageSettings,
+ variables: {
+ input: {
+ namespacePath: this.groupPath,
+ ...payload,
+ },
+ },
+ update: updateGroupPackageSettings(this.groupPath),
+ optimisticResponse: updateGroupPackagesSettingsOptimisticResponse({
+ ...this.packageSettings,
+ ...payload,
+ }),
+ })
+ .then(({ data }) => {
+ if (data.updateNamespacePackageSettings?.errors?.length > 0) {
+ this.alertMessage = ERROR_UPDATING_SETTINGS;
+ } else {
+ this.dismissAlert();
+ this.$toast.show(SUCCESS_UPDATING_SETTINGS, { type: 'success' });
+ }
+ })
+ .catch((e) => {
+ if (e.graphQLErrors) {
+ e.graphQLErrors.forEach((error) => {
+ const [
+ {
+ path: [key],
+ message,
+ },
+ ] = error.extensions.problems;
+ this.errors = { ...this.errors, [key]: message };
+ });
+ }
+ this.alertMessage = ERROR_UPDATING_SETTINGS;
+ });
+ },
+ },
};
</script>
<template>
- <section></section>
+ <div>
+ <gl-alert v-if="alertMessage" variant="warning" class="gl-mt-4" @dismiss="dismissAlert">
+ {{ alertMessage }}
+ </gl-alert>
+
+ <settings-block :default-expanded="defaultExpanded">
+ <template #title> {{ $options.i18n.PACKAGE_SETTINGS_HEADER }}</template>
+ <template #description>
+ <span data-testid="description">
+ <gl-sprintf :message="$options.i18n.PACKAGE_SETTINGS_DESCRIPTION">
+ <template #link="{ content }">
+ <gl-link :href="$options.links.PACKAGES_DOCS_PATH" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ <template #default>
+ <maven-settings
+ :maven-duplicates-allowed="packageSettings.mavenDuplicatesAllowed"
+ :maven-duplicate-exception-regex="packageSettings.mavenDuplicateExceptionRegex"
+ :maven-duplicate-exception-regex-error="errors.mavenDuplicateExceptionRegex"
+ :loading="isLoading"
+ @update="updateSettings"
+ />
+ </template>
+ </settings-block>
+ </div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue
new file mode 100644
index 00000000000..aa7c6e9d8d6
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue
@@ -0,0 +1,113 @@
+<script>
+import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui';
+
+import {
+ MAVEN_TITLE,
+ MAVEN_SETTINGS_SUBTITLE,
+ MAVEN_DUPLICATES_ALLOWED_DISABLED,
+ MAVEN_DUPLICATES_ALLOWED_ENABLED,
+ MAVEN_SETTING_EXCEPTION_TITLE,
+ MAVEN_SETTINGS_EXCEPTION_LEGEND,
+ MAVEN_DUPLICATES_ALLOWED,
+ MAVEN_DUPLICATE_EXCEPTION_REGEX,
+} from '~/packages_and_registries/settings/group/constants';
+
+export default {
+ name: 'MavenSettings',
+ i18n: {
+ MAVEN_TITLE,
+ MAVEN_SETTINGS_SUBTITLE,
+ MAVEN_SETTING_EXCEPTION_TITLE,
+ MAVEN_SETTINGS_EXCEPTION_LEGEND,
+ },
+ modelNames: {
+ MAVEN_DUPLICATES_ALLOWED,
+ MAVEN_DUPLICATE_EXCEPTION_REGEX,
+ },
+ components: {
+ GlSprintf,
+ GlToggle,
+ GlFormGroup,
+ GlFormInput,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ mavenDuplicatesAllowed: {
+ type: Boolean,
+ default: false,
+ required: true,
+ },
+ mavenDuplicateExceptionRegex: {
+ type: String,
+ default: '',
+ required: true,
+ },
+ mavenDuplicateExceptionRegexError: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ },
+ computed: {
+ enabledButtonLabel() {
+ return this.mavenDuplicatesAllowed
+ ? MAVEN_DUPLICATES_ALLOWED_ENABLED
+ : MAVEN_DUPLICATES_ALLOWED_DISABLED;
+ },
+ isMavenDuplicateExceptionRegexValid() {
+ return !this.mavenDuplicateExceptionRegexError;
+ },
+ },
+ methods: {
+ update(type, value) {
+ this.$emit('update', { [type]: value });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h5 class="gl-border-b-solid gl-border-b-1 gl-border-gray-200">
+ {{ $options.i18n.MAVEN_TITLE }}
+ </h5>
+ <p>{{ $options.i18n.MAVEN_SETTINGS_SUBTITLE }}</p>
+ <form>
+ <div class="gl-display-flex">
+ <gl-toggle
+ :value="mavenDuplicatesAllowed"
+ @change="update($options.modelNames.MAVEN_DUPLICATES_ALLOWED, $event)"
+ />
+ <div class="gl-ml-5">
+ <div data-testid="toggle-label">
+ <gl-sprintf :message="enabledButtonLabel">
+ <template #bold="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </div>
+ <gl-form-group
+ v-if="!mavenDuplicatesAllowed"
+ class="gl-mt-4"
+ :label="$options.i18n.MAVEN_SETTING_EXCEPTION_TITLE"
+ label-size="sm"
+ :state="isMavenDuplicateExceptionRegexValid"
+ :invalid-feedback="mavenDuplicateExceptionRegexError"
+ :description="$options.i18n.MAVEN_SETTINGS_EXCEPTION_LEGEND"
+ label-for="maven-duplicated-settings-regex-input"
+ >
+ <gl-form-input
+ id="maven-duplicated-settings-regex-input"
+ :value="mavenDuplicateExceptionRegex"
+ @change="update($options.modelNames.MAVEN_DUPLICATE_EXCEPTION_REGEX, $event)"
+ />
+ </gl-form-group>
+ </div>
+ </div>
+ </form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
new file mode 100644
index 00000000000..72bec74060c
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -0,0 +1,31 @@
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__, __ } from '~/locale';
+
+export const PACKAGE_SETTINGS_HEADER = s__('PackageRegistry|Package Registry');
+export const PACKAGE_SETTINGS_DESCRIPTION = s__(
+ 'PackageRegistry|GitLab Packages allows organizations to utilize GitLab as a private repository for a variety of common package formats. %{linkStart}More Information%{linkEnd}',
+);
+
+export const MAVEN_TITLE = s__('PackageRegistry|Maven');
+export const MAVEN_SETTINGS_SUBTITLE = s__('PackageRegistry|Settings for Maven packages');
+export const MAVEN_DUPLICATES_ALLOWED_DISABLED = s__(
+ 'PackageRegistry|%{boldStart}Do not allow duplicates%{boldEnd} - Packages with the same name and version are rejected.',
+);
+export const MAVEN_DUPLICATES_ALLOWED_ENABLED = s__(
+ 'PackageRegistry|%{boldStart}Allow duplicates%{boldEnd} - Packages with the same name and version are accepted.',
+);
+export const MAVEN_SETTING_EXCEPTION_TITLE = __('Exceptions');
+export const MAVEN_SETTINGS_EXCEPTION_LEGEND = s__(
+ 'PackageRegistry|Packages can be published if their name or version matches this regex',
+);
+
+export const SUCCESS_UPDATING_SETTINGS = s__('PackageRegistry|Settings saved successfully');
+export const ERROR_UPDATING_SETTINGS = s__(
+ 'PackageRegistry|An error occurred while saving the settings',
+);
+
+// Parameters
+
+export const PACKAGES_DOCS_PATH = helpPagePath('user/packages');
+export const MAVEN_DUPLICATES_ALLOWED = 'mavenDuplicatesAllowed';
+export const MAVEN_DUPLICATE_EXCEPTION_REGEX = 'mavenDuplicateExceptionRegex';
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
new file mode 100644
index 00000000000..1fc59bd3496
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateNamespacePackageSettings($input: UpdateNamespacePackageSettingsInput!) {
+ updateNamespacePackageSettings(input: $input) {
+ packageSettings {
+ mavenDuplicatesAllowed
+ mavenDuplicateExceptionRegex
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
new file mode 100644
index 00000000000..2011659887d
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
@@ -0,0 +1,8 @@
+query getGroupPackagesSettings($fullPath: ID!) {
+ group(fullPath: $fullPath) {
+ packageSettings {
+ mavenDuplicatesAllowed
+ mavenDuplicateExceptionRegex
+ }
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js
new file mode 100644
index 00000000000..06b57fe013f
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js
@@ -0,0 +1,22 @@
+import { produce } from 'immer';
+import getGroupPackagesSettingsQuery from '../queries/get_group_packages_settings.query.graphql';
+
+export const updateGroupPackageSettings = (fullPath) => (client, { data: updatedData }) => {
+ const queryAndParams = {
+ query: getGroupPackagesSettingsQuery,
+ variables: { fullPath },
+ };
+ const sourceData = client.readQuery(queryAndParams);
+
+ const data = produce(sourceData, (draftState) => {
+ // eslint-disable-next-line no-param-reassign
+ draftState.group.packageSettings = {
+ ...updatedData.updateNamespacePackageSettings.packageSettings,
+ };
+ });
+
+ client.writeQuery({
+ ...queryAndParams,
+ data,
+ });
+};
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js
new file mode 100644
index 00000000000..f2c8de85bf8
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js
@@ -0,0 +1,11 @@
+export const updateGroupPackagesSettingsOptimisticResponse = (changes) => ({
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Mutation',
+ updateNamespacePackageSettings: {
+ __typename: 'UpdateNamespacePackageSettingsPayload',
+ errors: [],
+ packageSettings: {
+ ...changes,
+ },
+ },
+});
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index f9a91ec322b..f78d2b0dbd3 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import 'vendor/jquery.endless-scroll';
-import { getParameterByName } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
+import { getParameterByName } from '~/lib/utils/common_utils';
import { removeParams } from '~/lib/utils/url_utility';
const ENDLESS_SCROLL_BOTTOM_PX = 400;
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
index da7f81759ea..e78b3f9ec95 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { truncate } from '../../../lib/utils/text_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { truncate } from '../../../lib/utils/text_utility';
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js
index d97e24d9e0b..5649c47d7e8 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/index.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js
@@ -1,6 +1,6 @@
/* eslint-disable no-new */
-import AbuseReports from './abuse_reports';
import UsersSelect from '~/users_select';
+import AbuseReports from './abuse_reports';
document.addEventListener('DOMContentLoaded', () => {
new AbuseReports();
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/general/index.js
index af1595398a8..f7bd32880ff 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js
@@ -1,6 +1,10 @@
+// This is a true violation of @gitlab/no-runtime-template-compiler, as it
+// relies on app/views/admin/application_settings/_gitpod.html.haml for its
+// template.
+/* eslint-disable @gitlab/no-runtime-template-compiler */
import Vue from 'vue';
-import initUserInternalRegexPlaceholder from '../account_and_limits';
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
+import initUserInternalRegexPlaceholder from '../account_and_limits';
document.addEventListener('DOMContentLoaded', () => {
initUserInternalRegexPlaceholder();
diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js
index cce30e6b12a..e7e74588bec 100644
--- a/app/assets/javascripts/pages/admin/application_settings/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/index.js
@@ -1,7 +1,8 @@
-import initSettingsPanels from '~/settings_panels';
+import initVariableList from '~/ci_variable_list';
import projectSelect from '~/project_select';
+import initSearchSettings from '~/search_settings';
import selfMonitor from '~/self_monitor';
-import initVariableList from '~/ci_variable_list';
+import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
if (gon.features?.ciInstanceVariablesUi) {
@@ -11,4 +12,5 @@ document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
projectSelect();
+ initSearchSettings();
});
diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
index b995cb1d3dd..bc1d4dd6122 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
@@ -1,6 +1,6 @@
+import { deprecatedCreateFlash as flash } from '../../../flash';
import axios from '../../../lib/utils/axios_utils';
import { __ } from '../../../locale';
-import { deprecatedCreateFlash as flash } from '../../../flash';
export default class PayloadPreviewer {
constructor(trigger) {
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
index 9e4c4d9f615..5a16716fe2d 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import { debounce } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
-import { __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
import { textColorForBackground } from '~/lib/utils/color_utils';
+import { __ } from '~/locale';
export default () => {
const $broadcastMessageColor = $('.js-broadcast-message-color');
diff --git a/app/assets/javascripts/pages/admin/clusters/index/index.js b/app/assets/javascripts/pages/admin/clusters/index/index.js
index 744be65bfbe..a99e0dfa4f0 100644
--- a/app/assets/javascripts/pages/admin/clusters/index/index.js
+++ b/app/assets/javascripts/pages/admin/clusters/index/index.js
@@ -1,5 +1,5 @@
-import PersistentUserCallout from '~/persistent_user_callout';
import initClustersListApp from '~/clusters_list';
+import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
const callout = document.querySelector('.gcp-signup-offer');
diff --git a/app/assets/javascripts/pages/admin/clusters/show/index.js b/app/assets/javascripts/pages/admin/clusters/show/index.js
index f87da6c7074..9d94973af0d 100644
--- a/app/assets/javascripts/pages/admin/clusters/show/index.js
+++ b/app/assets/javascripts/pages/admin/clusters/show/index.js
@@ -1,6 +1,6 @@
import ClustersBundle from '~/clusters/clusters_bundle';
-import initClusterHealth from '~/pages/projects/clusters/show/cluster_health';
import initIntegrationForm from '~/clusters/forms/show';
+import initClusterHealth from '~/pages/projects/clusters/show/cluster_health';
document.addEventListener('DOMContentLoaded', () => {
new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js
deleted file mode 100644
index 1cc54df15a1..00000000000
--- a/app/assets/javascripts/pages/admin/cohorts/index.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import Vue from 'vue';
-import UsagePingDisabled from '~/admin/cohorts/components/usage_ping_disabled.vue';
-
-document.addEventListener('DOMContentLoaded', () => {
- const emptyStateContainer = document.getElementById('js-cohorts-empty-state');
-
- if (!emptyStateContainer) return false;
-
- const { emptyStateSvgPath, enableUsagePingLink, docsLink } = emptyStateContainer.dataset;
-
- return new Vue({
- el: emptyStateContainer,
- provide: {
- svgPath: emptyStateSvgPath,
- primaryButtonPath: enableUsagePingLink,
- docsLink,
- },
- render(h) {
- return h(UsagePingDisabled);
- },
- });
-});
diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js
index b94c999ed12..94f7cfd55be 100644
--- a/app/assets/javascripts/pages/admin/groups/new/index.js
+++ b/app/assets/javascripts/pages/admin/groups/new/index.js
@@ -1,6 +1,6 @@
+import initFilePickers from '~/file_pickers';
import BindInOut from '../../../../behaviors/bind_in_out';
import Group from '../../../../group';
-import initFilePickers from '~/file_pickers';
document.addEventListener('DOMContentLoaded', () => {
BindInOut.initAll();
diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js
index 3f4e658fc8d..792a6eda14e 100644
--- a/app/assets/javascripts/pages/admin/index.js
+++ b/app/assets/javascripts/pages/admin/index.js
@@ -1,6 +1,6 @@
-import initAdmin from './admin';
import initAdminStatisticsPanel from '../../admin/statistics_panel/index';
import initVueAlerts from '../../vue_alerts';
+import initAdmin from './admin';
document.addEventListener('DOMContentLoaded', initVueAlerts);
diff --git a/app/assets/javascripts/pages/admin/integrations/edit/index.js b/app/assets/javascripts/pages/admin/integrations/edit/index.js
index 2d77f2686f7..ba4b271f09e 100644
--- a/app/assets/javascripts/pages/admin/integrations/edit/index.js
+++ b/app/assets/javascripts/pages/admin/integrations/edit/index.js
@@ -1,6 +1,5 @@
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
-import initAlertsSettings from '~/alerts_service_settings';
document.addEventListener('DOMContentLoaded', () => {
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
@@ -11,6 +10,4 @@ document.addEventListener('DOMContentLoaded', () => {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
}
-
- initAlertsSettings(document.querySelector('.js-alerts-service-settings'));
});
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index d65593963ce..798eeee48bf 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlModal } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js
index 4df210debb5..46ddb95299d 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/index.js
+++ b/app/assets/javascripts/pages/admin/jobs/index/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Translate from '~/vue_shared/translate';
import stopJobsModal from './components/stop_jobs_modal.vue';
@@ -18,7 +19,7 @@ document.addEventListener('DOMContentLoaded', () => {
mounted() {
stopJobsButton.classList.remove('disabled');
stopJobsButton.addEventListener('click', () => {
- this.$root.$emit('bv::show::modal', modalId, `#${buttonId}`);
+ this.$root.$emit(BV_SHOW_MODAL, modalId, `#${buttonId}`);
});
},
render(createElement) {
diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js
index fa2b0546c02..2286a085143 100644
--- a/app/assets/javascripts/pages/admin/projects/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import ProjectsList from '~/projects_list';
import NamespaceSelect from '~/namespace_select';
+import ProjectsList from '~/projects_list';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
function mountRemoveMemberModal() {
diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js
index bf512ef395d..1971323abac 100644
--- a/app/assets/javascripts/pages/admin/projects/index/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index/index.js
@@ -1,7 +1,8 @@
import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import csrf from '~/lib/utils/csrf';
+import Translate from '~/vue_shared/translate';
import deleteProjectModal from './components/delete_project_modal.vue';
@@ -24,7 +25,7 @@ document.addEventListener('DOMContentLoaded', () => {
deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl;
deleteModal.projectName = buttonProps.projectName;
- this.$root.$emit('bv::show::modal', 'delete-project-modal');
+ this.$root.$emit(BV_SHOW_MODAL, 'delete-project-modal');
});
});
},
diff --git a/app/assets/javascripts/pages/admin/runners/index.js b/app/assets/javascripts/pages/admin/runners/index.js
index e60c6133c7c..f07ac1d8674 100644
--- a/app/assets/javascripts/pages/admin/runners/index.js
+++ b/app/assets/javascripts/pages/admin/runners/index.js
@@ -1,6 +1,7 @@
-import initFilteredSearch from '~/pages/search/init_filtered_search';
import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
@@ -8,4 +9,6 @@ document.addEventListener('DOMContentLoaded', () => {
filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys,
useDefaultState: true,
});
+
+ initInstallRunner();
});
diff --git a/app/assets/javascripts/pages/admin/services/edit/index.js b/app/assets/javascripts/pages/admin/services/edit/index.js
index e5e80d2f566..3d692ef4dcc 100644
--- a/app/assets/javascripts/pages/admin/services/edit/index.js
+++ b/app/assets/javascripts/pages/admin/services/edit/index.js
@@ -1,9 +1,6 @@
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
-import initAlertsSettings from '~/alerts_service_settings';
document.addEventListener('DOMContentLoaded', () => {
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
-
- initAlertsSettings(document.querySelector('.js-alerts-service-settings'));
});
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 9c303cc6445..d2b83f980d7 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -85,38 +85,36 @@ export default {
<template>
<gl-modal ref="modal" modal-id="delete-user-modal" :title="modalTitle" kind="danger">
- <template>
- <p>
- <gl-sprintf :message="content">
- <template #username>
- <strong>{{ username }}</strong>
- </template>
- <template #strong="props">
- <strong>{{ props.content }}</strong>
- </template>
- </gl-sprintf>
- </p>
+ <p>
+ <gl-sprintf :message="content">
+ <template #username>
+ <strong>{{ username }}</strong>
+ </template>
+ <template #strong="props">
+ <strong>{{ props.content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
- <p>
- <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
- <template #username>
- <code>{{ username }}</code>
- </template>
- </gl-sprintf>
- </p>
+ <p>
+ <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
+ <template #username>
+ <code>{{ username }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
- <form ref="form" :action="deleteUserUrl" method="post" @submit.prevent>
- <input ref="method" type="hidden" name="_method" value="delete" />
- <input :value="csrfToken" type="hidden" name="authenticity_token" />
- <gl-form-input
- v-model="enteredUsername"
- autofocus
- type="text"
- name="username"
- autocomplete="off"
- />
- </form>
- </template>
+ <form ref="form" :action="deleteUserUrl" method="post" @submit.prevent>
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <gl-form-input
+ v-model="enteredUsername"
+ autofocus
+ type="text"
+ name="username"
+ autocomplete="off"
+ />
+ </form>
<template #modal-footer>
<gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button>
<gl-button
diff --git a/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue b/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue
index 24c9fa4cb3f..1dfea3f1e7b 100644
--- a/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue
+++ b/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue
@@ -12,6 +12,10 @@ export default {
required: true,
type: String,
},
+ selector: {
+ required: true,
+ type: String,
+ },
},
data() {
return {
@@ -34,22 +38,24 @@ export default {
},
mounted() {
- document.addEventListener('click', this.handleClick);
- },
+ /*
+ * Here we're looking for every button that needs to launch a modal
+ * on click, and then attaching a click event handler to show the modal
+ * if it's correctly configured.
+ *
+ * TODO: Replace this with integrated modal components https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+ */
+ document.querySelectorAll(this.selector).forEach((button) => {
+ button.addEventListener('click', (e) => {
+ if (!button.dataset.glModalAction) return;
- beforeDestroy() {
- document.removeEventListener('click', this.handleClick);
+ e.preventDefault();
+ this.show(button.dataset);
+ });
+ });
},
methods: {
- handleClick(e) {
- const { glModalAction: action } = e.target.dataset;
- if (!action) return;
-
- this.show(e.target.dataset);
- e.preventDefault();
- },
-
show(modalData) {
const { glModalAction: requestedAction } = modalData;
diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js
index 75a8284f5f8..b1079c3b068 100644
--- a/app/assets/javascripts/pages/admin/users/index.js
+++ b/app/assets/javascripts/pages/admin/users/index.js
@@ -1,11 +1,13 @@
import Vue from 'vue';
+import { initAdminUsersApp, initCohortsEmptyState } from '~/admin/users';
+import initTabs from '~/admin/users/tabs';
+import initConfirmModal from '~/confirm_modal';
+import csrf from '~/lib/utils/csrf';
import Translate from '~/vue_shared/translate';
import ModalManager from './components/user_modal_manager.vue';
-import csrf from '~/lib/utils/csrf';
-import initConfirmModal from '~/confirm_modal';
-import initAdminUsersApp from '~/admin/users';
+const CONFIRM_DELETE_BUTTON_SELECTOR = '.js-delete-user-modal-button';
const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts';
const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal';
@@ -32,6 +34,8 @@ function loadModalsConfigurationFromHtml(modalsElement) {
document.addEventListener('DOMContentLoaded', () => {
Vue.use(Translate);
+ initAdminUsersApp();
+
const modalConfiguration = loadModalsConfigurationFromHtml(
document.querySelector(MODAL_TEXTS_CONTAINER_SELECTOR),
);
@@ -49,6 +53,7 @@ document.addEventListener('DOMContentLoaded', () => {
return h(ModalManager, {
ref: 'manager',
props: {
+ selector: CONFIRM_DELETE_BUTTON_SELECTOR,
modalConfiguration,
csrfToken: csrf.token,
},
@@ -57,5 +62,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
initConfirmModal();
- initAdminUsersApp();
+ initCohortsEmptyState();
+ initTabs();
});
diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js
index 71cdaf45052..3ad95fb1318 100644
--- a/app/assets/javascripts/pages/dashboard/issues/index.js
+++ b/app/assets/javascripts/pages/dashboard/issues/index.js
@@ -1,8 +1,8 @@
-import projectSelect from '~/project_select';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import { FILTERED_SEARCH } from '~/pages/constants';
import initManualOrdering from '~/manual_ordering';
+import { FILTERED_SEARCH } from '~/pages/constants';
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import projectSelect from '~/project_select';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
index 7adae2cdb05..b099165e3f5 100644
--- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js
+++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
@@ -1,8 +1,8 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
-import projectSelect from '~/project_select';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import projectSelect from '~/project_select';
document.addEventListener('DOMContentLoaded', () => {
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, true);
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
index bed753b0c40..d17c37e9e1a 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
+++ b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
@@ -1,7 +1,7 @@
<script>
import { GlBanner } from '@gitlab/ui';
-import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index bd283201eff..d53cd405504 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -2,13 +2,13 @@
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { visitUrl } from '~/lib/utils/url_utility';
-import UsersSelect from '~/users_select';
+import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { isMetaClick } from '~/lib/utils/common_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
+import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import { deprecatedCreateFlash as flash } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
+import UsersSelect from '~/users_select';
export default class Todos {
constructor() {
diff --git a/app/assets/javascripts/pages/groups/boards/index.js b/app/assets/javascripts/pages/groups/boards/index.js
index 922f39627c9..bde0007ec6a 100644
--- a/app/assets/javascripts/pages/groups/boards/index.js
+++ b/app/assets/javascripts/pages/groups/boards/index.js
@@ -1,6 +1,6 @@
-import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initBoards from '~/boards';
+import UsersSelect from '~/users_select';
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/groups/clusters/index.js b/app/assets/javascripts/pages/groups/clusters/index.js
index 9f466e0d60a..d5ce5d076a2 100644
--- a/app/assets/javascripts/pages/groups/clusters/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/index.js
@@ -1,5 +1,5 @@
-import initCreateCluster from '~/create_cluster/init_create_cluster';
import initIntegrationForm from '~/clusters/forms/show/index';
+import initCreateCluster from '~/create_cluster/init_create_cluster';
document.addEventListener('DOMContentLoaded', () => {
initCreateCluster(document, gon);
diff --git a/app/assets/javascripts/pages/groups/clusters/index/index.js b/app/assets/javascripts/pages/groups/clusters/index/index.js
index 744be65bfbe..a99e0dfa4f0 100644
--- a/app/assets/javascripts/pages/groups/clusters/index/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/index/index.js
@@ -1,5 +1,5 @@
-import PersistentUserCallout from '~/persistent_user_callout';
import initClustersListApp from '~/clusters_list';
+import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
const callout = document.querySelector('.gcp-signup-offer');
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 33e552cd1ba..95ee512b71a 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -1,13 +1,13 @@
-import initFilePickers from '~/file_pickers';
-import TransferDropdown from '~/groups/transfer_dropdown';
+import { GROUP_BADGE } from '~/badges/constants';
import initConfirmDangerModal from '~/confirm_danger_modal';
-import initSettingsPanels from '~/settings_panels';
-import setupTransferEdit from '~/transfer_edit';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
-import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
-import { GROUP_BADGE } from '~/badges/constants';
+import initFilePickers from '~/file_pickers';
+import TransferDropdown from '~/groups/transfer_dropdown';
import groupsSelect from '~/groups_select';
+import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import projectSelect from '~/project_select';
+import initSettingsPanels from '~/settings_panels';
+import setupTransferEdit from '~/transfer_edit';
document.addEventListener('DOMContentLoaded', () => {
initFilePickers();
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index 5346e3720e8..3496f699b06 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -1,13 +1,14 @@
import Vue from 'vue';
-import memberExpirationDate from '~/member_expiration_date';
-import UsersSelect from '~/users_select';
+import { groupMemberRequestFormatter } from '~/groups/members/utils';
import groupsSelect from '~/groups_select';
-import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
-import { initGroupMembersApp } from '~/groups/members';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
-import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils';
import { s__ } from '~/locale';
+import memberExpirationDate from '~/member_expiration_date';
+import { initMembersApp } from '~/members/index';
+import { groupLinkRequestFormatter } from '~/members/utils';
+import UsersSelect from '~/users_select';
+import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
@@ -25,11 +26,11 @@ function mountRemoveMemberModal() {
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
-initGroupMembersApp(document.querySelector('.js-group-members-list'), {
+initMembersApp(document.querySelector('.js-group-members-list'), {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
- requestFormatter: memberRequestFormatter,
+ requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: {
show: true,
tokens: ['two_factor', 'with_inherited_permissions'],
@@ -38,7 +39,8 @@ initGroupMembersApp(document.querySelector('.js-group-members-list'), {
recentSearchesStorageKey: 'group_members',
},
});
-initGroupMembersApp(document.querySelector('.js-group-linked-list'), {
+
+initMembersApp(document.querySelector('.js-group-group-links-list'), {
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
@@ -46,9 +48,9 @@ initGroupMembersApp(document.querySelector('.js-group-linked-list'), {
},
requestFormatter: groupLinkRequestFormatter,
});
-initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), {
+initMembersApp(document.querySelector('.js-group-invited-members-list'), {
tableFields: SHARED_FIELDS.concat('invited'),
- requestFormatter: memberRequestFormatter,
+ requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: {
show: true,
tokens: [],
@@ -57,9 +59,9 @@ initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), {
recentSearchesStorageKey: 'group_invited_members',
},
});
-initGroupMembersApp(document.querySelector('.js-group-access-requests-list'), {
+initMembersApp(document.querySelector('.js-group-access-requests-list'), {
tableFields: SHARED_FIELDS.concat('requested'),
- requestFormatter: memberRequestFormatter,
+ requestFormatter: groupMemberRequestFormatter,
});
groupsSelect();
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 5cb21ca61ab..b60607e8857 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,10 +1,10 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
-import initIssuablesList from '~/issues_list';
-import projectSelect from '~/project_select';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
-import { FILTERED_SEARCH } from '~/pages/constants';
+import initIssuablesList from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
+import { FILTERED_SEARCH } from '~/pages/constants';
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import projectSelect from '~/project_select';
const ISSUE_BULK_UPDATE_PREFIX = 'issue_';
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index 2832cbed5ac..2f6f9bb16e1 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -1,9 +1,9 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
-import projectSelect from '~/project_select';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
-import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/pages/constants';
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import projectSelect from '~/project_select';
const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js
index ebaea5ef3dc..3094fe5cd21 100644
--- a/app/assets/javascripts/pages/groups/milestones/show/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/show/index.js
@@ -1,7 +1,6 @@
-import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
-import initDeleteMilestoneModal from '~/pages/milestones/shared/delete_milestone_modal_init';
-
import Milestone from '~/milestone';
+import initDeleteMilestoneModal from '~/pages/milestones/shared/delete_milestone_modal_init';
+import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
document.addEventListener('DOMContentLoaded', () => {
initMilestonesShow();
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
index 97f3d8cf7f5..89dccea2812 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -1,9 +1,9 @@
import { debounce } from 'lodash';
-import InputValidator from '~/validators/input_validator';
-import fetchGroupPathAvailability from './fetch_group_path_availability';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
+import InputValidator from '~/validators/input_validator';
+import fetchGroupPathAvailability from './fetch_group_path_availability';
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 7021473b380..63515abe698 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import BindInOut from '~/behaviors/bind_in_out';
+import initFilePickers from '~/file_pickers';
import Group from '~/group';
import GroupPathValidator from './group_path_validator';
-import initFilePickers from '~/file_pickers';
const parentId = $('#group_parent_id');
if (!parentId.val()) {
diff --git a/app/assets/javascripts/pages/groups/settings/badges/index.js b/app/assets/javascripts/pages/groups/settings/badges/index.js
index 74e96ee4a8f..3f48e4f281e 100644
--- a/app/assets/javascripts/pages/groups/settings/badges/index.js
+++ b/app/assets/javascripts/pages/groups/settings/badges/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
import { GROUP_BADGE } from '~/badges/constants';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
+import Translate from '~/vue_shared/translate';
Vue.use(Translate);
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index e8d8c985ade..378b8663777 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -1,9 +1,10 @@
-import initSettingsPanels from '~/settings_panels';
import initVariableList from '~/ci_variable_list';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners_filtered_search_token_keys';
-import { FILTERED_SEARCH } from '~/pages/constants';
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
+import { FILTERED_SEARCH } from '~/pages/constants';
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
+import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
@@ -18,4 +19,6 @@ document.addEventListener('DOMContentLoaded', () => {
initSharedRunnersForm();
initVariableList();
+
+ initInstallRunner();
});
diff --git a/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js b/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js
index 2d77f2686f7..ba4b271f09e 100644
--- a/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js
+++ b/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js
@@ -1,6 +1,5 @@
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
-import initAlertsSettings from '~/alerts_service_settings';
document.addEventListener('DOMContentLoaded', () => {
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
@@ -11,6 +10,4 @@ document.addEventListener('DOMContentLoaded', () => {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
}
-
- initAlertsSettings(document.querySelector('.js-alerts-service-settings'));
});
diff --git a/app/assets/javascripts/pages/groups/settings/repository/show/index.js b/app/assets/javascripts/pages/groups/settings/repository/show/index.js
index f4b26ba81fe..a1bcf6dbf57 100644
--- a/app/assets/javascripts/pages/groups/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/repository/show/index.js
@@ -1,5 +1,5 @@
-import initSettingsPanels from '~/settings_panels';
import DueDateSelectors from '~/due_date_select';
+import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 8d956c694c0..8c272e561db 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -1,13 +1,16 @@
/* eslint-disable no-new */
-import { getPagePath, getDashPath } from '~/lib/utils/common_utils';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
+import initInviteMembersBanner from '~/groups/init_invite_members_banner';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
+import { getPagePath, getDashPath } from '~/lib/utils/common_utils';
+import initNotificationsDropdown from '~/notifications';
import notificationsDropdown from '~/notifications_dropdown';
import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
-import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GroupTabs from './group_tabs';
-import initInviteMembersBanner from '~/groups/init_invite_members_banner';
export default function initGroupDetails(actionName = 'show') {
const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED];
@@ -20,8 +23,16 @@ export default function initGroupDetails(actionName = 'show') {
new GroupTabs({ parentEl: '.groups-listing', action });
new ShortcutsNavigation();
new NotificationsForm();
- notificationsDropdown();
+
+ if (gon.features?.vueNotificationDropdown) {
+ initNotificationsDropdown();
+ } else {
+ notificationsDropdown();
+ }
+
new ProjectsList();
initInviteMembersBanner();
+ initInviteMembersModal();
+ initInviteMembersTrigger();
}
diff --git a/app/assets/javascripts/pages/groups/shared/group_tabs.js b/app/assets/javascripts/pages/groups/shared/group_tabs.js
index 033843d8504..73d810007dc 100644
--- a/app/assets/javascripts/pages/groups/shared/group_tabs.js
+++ b/app/assets/javascripts/pages/groups/shared/group_tabs.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import { removeParams } from '~/lib/utils/url_utility';
import createGroupTree from '~/groups';
import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
@@ -9,8 +8,9 @@ import {
GROUPS_LIST_HOLDER_CLASS,
GROUPS_FILTER_FORM_CLASS,
} from '~/groups/constants';
-import UserTabs from '~/pages/users/user_tabs';
import GroupFilterableList from '~/groups/groups_filterable_list';
+import { removeParams } from '~/lib/utils/url_utility';
+import UserTabs from '~/pages/users/user_tabs';
export default class GroupTabs extends UserTabs {
constructor({ defaultAction = 'subgroups_and_projects', action, parentEl }) {
diff --git a/app/assets/javascripts/pages/help/index/index.js b/app/assets/javascripts/pages/help/index/index.js
index 1bafe564a37..fd8c590eb4e 100644
--- a/app/assets/javascripts/pages/help/index/index.js
+++ b/app/assets/javascripts/pages/help/index/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import VersionCheckImage from '~/version_check_image';
import docs from '~/docs/docs_bundle';
+import VersionCheckImage from '~/version_check_image';
document.addEventListener('DOMContentLoaded', () => {
docs();
diff --git a/app/assets/javascripts/pages/import/bulk_imports/index.js b/app/assets/javascripts/pages/import/bulk_imports/status/index.js
index 37ac1a98466..37ac1a98466 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/index.js
+++ b/app/assets/javascripts/pages/import/bulk_imports/status/index.js
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index f47945c5a9f..16f68b94c9a 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -1,10 +1,10 @@
<script>
import { GlSafeHtmlDirective as SafeHtml, GlModal } from '@gitlab/ui';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as Flash } from '~/flash';
-import { __, n__, s__, sprintf } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
+import { __, n__, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
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 ecde11aff40..8d4997586dd 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,9 +1,9 @@
<script>
import { GlModal } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__, sprintf } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
+import { s__, sprintf } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
index e8b67891c42..3aeff2db2e0 100644
--- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
+++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Translate from '~/vue_shared/translate';
import DeleteMilestoneModal from './components/delete_milestone_modal.vue';
import eventHub from './event_hub';
@@ -47,7 +48,7 @@ export default () => {
deleteMilestoneButtons.forEach((button) => {
button.removeAttribute('disabled');
button.addEventListener('click', () => {
- this.$root.$emit('bv::show::modal', 'delete-milestone-modal');
+ this.$root.$emit(BV_SHOW_MODAL, 'delete-milestone-modal');
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
this.setModalProps({
diff --git a/app/assets/javascripts/pages/oauth/index.js b/app/assets/javascripts/pages/oauth/index.js
new file mode 100644
index 00000000000..2a120a690ef
--- /dev/null
+++ b/app/assets/javascripts/pages/oauth/index.js
@@ -0,0 +1,3 @@
+import initSearchSettings from '~/search_settings';
+
+initSearchSettings();
diff --git a/app/assets/javascripts/pages/profiles/accounts/show/index.js b/app/assets/javascripts/pages/profiles/accounts/show/index.js
index 6c1e953aa83..5350ef61184 100644
--- a/app/assets/javascripts/pages/profiles/accounts/show/index.js
+++ b/app/assets/javascripts/pages/profiles/accounts/show/index.js
@@ -1,5 +1,5 @@
-import initProfileAccount from '~/profile/account';
import { initClose2faSuccessMessage } from '~/authentication/two_factor_auth';
+import initProfileAccount from '~/profile/account';
document.addEventListener('DOMContentLoaded', initProfileAccount);
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
index d2b00d0ef45..535fe5fa4eb 100644
--- a/app/assets/javascripts/pages/profiles/index.js
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import '~/profile/gl_crop';
import Profile from '~/profile/profile';
+import initSearchSettings from '~/search_settings';
document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line func-names
@@ -17,4 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
new Profile(); // eslint-disable-line no-new
+
+ initSearchSettings();
});
diff --git a/app/assets/javascripts/pages/profiles/index/index.js b/app/assets/javascripts/pages/profiles/index/index.js
index 9bd430f4f11..586b3be8661 100644
--- a/app/assets/javascripts/pages/profiles/index/index.js
+++ b/app/assets/javascripts/pages/profiles/index/index.js
@@ -1,5 +1,5 @@
-import NotificationsForm from '../../../notifications_form';
import notificationsDropdown from '../../../notifications_dropdown';
+import NotificationsForm from '../../../notifications_form';
document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/profiles/notifications/show/index.js b/app/assets/javascripts/pages/profiles/notifications/show/index.js
index 2e24a10fa5c..639f5deb72c 100644
--- a/app/assets/javascripts/pages/profiles/notifications/show/index.js
+++ b/app/assets/javascripts/pages/profiles/notifications/show/index.js
@@ -1,7 +1,9 @@
-import NotificationsForm from '../../../../notifications_form';
+import initNotificationsDropdown from '~/notifications';
import notificationsDropdown from '../../../../notifications_dropdown';
+import NotificationsForm from '../../../../notifications_form';
document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
+ initNotificationsDropdown();
});
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index b78f24ca2fb..a24c6ca7754 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -1,10 +1,10 @@
+import emojiRegex from 'emoji-regex';
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
-import emojiRegex from 'emoji-regex';
+import * as Emoji from '~/emoji';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import EmojiMenu from './emoji_menu';
import { __ } from '~/locale';
-import * as Emoji from '~/emoji';
+import EmojiMenu from './emoji_menu';
const defaultStatusEmoji = 'speech_balloon';
diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index 24dbc312dd2..186072531b8 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -1,6 +1,6 @@
-import { parseBoolean } from '~/lib/utils/common_utils';
import { mount2faRegistration } from '~/authentication/mount_2fa';
import { initRecoveryCodes } from '~/authentication/two_factor_auth';
+import { parseBoolean } from '~/lib/utils/common_utils';
document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js
index d39ea3d10bf..03fbad0f1ec 100644
--- a/app/assets/javascripts/pages/projects/activity/index.js
+++ b/app/assets/javascripts/pages/projects/activity/index.js
@@ -1,7 +1,5 @@
import Activities from '~/activities';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-document.addEventListener('DOMContentLoaded', () => {
- new Activities(); // eslint-disable-line no-new
- new ShortcutsNavigation(); // eslint-disable-line no-new
-});
+new Activities(); // eslint-disable-line no-new
+new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/alert_management/details/index.js b/app/assets/javascripts/pages/projects/alert_management/details/index.js
index a20f6713c9d..183e07ca1fc 100644
--- a/app/assets/javascripts/pages/projects/alert_management/details/index.js
+++ b/app/assets/javascripts/pages/projects/alert_management/details/index.js
@@ -1,3 +1,3 @@
-import AlertDetails from '~/alert_management/details';
+import AlertDetails from '~/vue_shared/alert_details';
AlertDetails('#js-alert_details');
diff --git a/app/assets/javascripts/pages/projects/artifacts/browse/index.js b/app/assets/javascripts/pages/projects/artifacts/browse/index.js
index 26dc90a56d7..58ba6a500a3 100644
--- a/app/assets/javascripts/pages/projects/artifacts/browse/index.js
+++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js
@@ -1,5 +1,5 @@
-import BuildArtifacts from '~/build_artifacts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
+import BuildArtifacts from '~/build_artifacts';
document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js
index 249900d6cb7..eb5ecc27c43 100644
--- a/app/assets/javascripts/pages/projects/artifacts/file/index.js
+++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js
@@ -1,5 +1,5 @@
-import BlobViewer from '~/blob/viewer/index';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
+import BlobViewer from '~/blob/viewer/index';
document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/blame/show/index.js b/app/assets/javascripts/pages/projects/blame/show/index.js
index 80d0bff92fa..fa22c11d1d7 100644
--- a/app/assets/javascripts/pages/projects/blame/show/index.js
+++ b/app/assets/javascripts/pages/projects/blame/show/index.js
@@ -1,3 +1,3 @@
import initBlob from '~/pages/projects/init_blob';
-document.addEventListener('DOMContentLoaded', initBlob);
+initBlob();
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 57c4ffd3933..61ff1c95a38 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
-import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
import BlobViewer from '~/blob/viewer/index';
-import initBlob from '~/pages/projects/init_blob';
import GpgBadges from '~/gpg_badges';
+import initBlob from '~/pages/projects/init_blob';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
+import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import '~/sourcegraph/load';
-import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js
index 79c3be771d0..3a06d0faa3e 100644
--- a/app/assets/javascripts/pages/projects/boards/index.js
+++ b/app/assets/javascripts/pages/projects/boards/index.js
@@ -1,6 +1,6 @@
-import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initBoards from '~/boards';
+import UsersSelect from '~/users_select';
document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js
index 1124eb5d939..2b5451bd18b 100644
--- a/app/assets/javascripts/pages/projects/clusters/index/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index/index.js
@@ -1,8 +1,6 @@
import initClustersListApp from 'ee_else_ce/clusters_list';
import PersistentUserCallout from '~/persistent_user_callout';
-document.addEventListener('DOMContentLoaded', () => {
- const callout = document.querySelector('.gcp-signup-offer');
- PersistentUserCallout.factory(callout);
- initClustersListApp();
-});
+const callout = document.querySelector('.gcp-signup-offer');
+PersistentUserCallout.factory(callout);
+initClustersListApp();
diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js
index 876bab0b339..de9ded87ef3 100644
--- a/app/assets/javascripts/pages/projects/clusters/new/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/new/index.js
@@ -1,5 +1,3 @@
import initNewCluster from '~/clusters/new_cluster';
-document.addEventListener('DOMContentLoaded', () => {
- initNewCluster();
-});
+initNewCluster();
diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js
index a05ea8ae845..1d019285e23 100644
--- a/app/assets/javascripts/pages/projects/clusters/show/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/show/index.js
@@ -1,7 +1,7 @@
import ClustersBundle from '~/clusters/clusters_bundle';
+import initIntegrationForm from '~/clusters/forms/show';
import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
import initClusterHealth from './cluster_health';
-import initIntegrationForm from '~/clusters/forms/show';
document.addEventListener('DOMContentLoaded', () => {
new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
index eaf340f2725..6efd8298bf8 100644
--- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js
+++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
@@ -1,5 +1,7 @@
-import { initCommitBoxInfo } from '~/projects/commit_box/info';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import initCommitActions from '~/projects/commit';
+import { initCommitBoxInfo } from '~/projects/commit_box/info';
initCommitBoxInfo();
initPipelines();
+initCommitActions();
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 5cfdb125e4f..90a663802d2 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -1,21 +1,19 @@
/* eslint-disable no-new */
-
import $ from 'jquery';
-import Diff from '~/diff';
-import ZenMode from '~/zen_mode';
+import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import initNotes from '~/init_notes';
+import Diff from '~/diff';
+import flash from '~/flash';
import initChangesDropdown from '~/init_changes_dropdown';
-import '~/sourcegraph/load';
-import { handleLocationHash } from '~/lib/utils/common_utils';
+import initNotes from '~/init_notes';
import axios from '~/lib/utils/axios_utils';
-import syntaxHighlight from '~/syntax_highlight';
-import flash from '~/flash';
+import { handleLocationHash } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import loadAwardsHandler from '~/awards_handler';
+import initCommitActions from '~/projects/commit';
import { initCommitBoxInfo } from '~/projects/commit_box/info';
-import initRevertCommitTrigger from '~/projects/commit/init_revert_commit_trigger';
-import initRevertCommitModal from '~/projects/commit/init_revert_commit_modal';
+import syntaxHighlight from '~/syntax_highlight';
+import ZenMode from '~/zen_mode';
+import '~/sourcegraph/load';
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
@@ -47,5 +45,4 @@ if (filesContainer.length) {
new Diff();
}
loadAwardsHandler();
-initRevertCommitModal();
-initRevertCommitTrigger();
+initCommitActions();
diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js
index 6239e4c99d2..ee74628a994 100644
--- a/app/assets/javascripts/pages/projects/commits/show/index.js
+++ b/app/assets/javascripts/pages/projects/commits/show/index.js
@@ -1,6 +1,6 @@
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import CommitsList from '~/commits';
import GpgBadges from '~/gpg_badges';
-import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import mountCommits from '~/projects/commits';
new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/compare/index/index.js b/app/assets/javascripts/pages/projects/compare/index/index.js
new file mode 100644
index 00000000000..b86c9ec442f
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/compare/index/index.js
@@ -0,0 +1,3 @@
+import initCompareSelector from '~/projects/compare';
+
+initCompareSelector();
diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js
index a626ed2d30b..f1cf9caa28b 100644
--- a/app/assets/javascripts/pages/projects/compare/show/index.js
+++ b/app/assets/javascripts/pages/projects/compare/show/index.js
@@ -1,6 +1,6 @@
import Diff from '~/diff';
-import initChangesDropdown from '~/init_changes_dropdown';
import GpgBadges from '~/gpg_badges';
+import initChangesDropdown from '~/init_changes_dropdown';
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 5f1d3edc3ba..9aa7e62e3ee 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,36 +1,34 @@
import { PROJECT_BADGE } from '~/badges/constants';
-import initSettingsPanels from '~/settings_panels';
-import setupTransferEdit from '~/transfer_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
-import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
-import initProjectLoadingSpinner from '../shared/save_project_loader';
-import initProjectPermissionsSettings from '../shared/permissions';
+import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import initProjectDeleteButton from '~/projects/project_delete_button';
-import UserCallout from '~/user_callout';
import initServiceDesk from '~/projects/settings_service_desk';
-import mountSearchSettings from './mount_search_settings';
+import initSearchSettings from '~/search_settings';
+import initSettingsPanels from '~/settings_panels';
+import setupTransferEdit from '~/transfer_edit';
+import UserCallout from '~/user_callout';
+import initProjectPermissionsSettings from '../shared/permissions';
+import initProjectLoadingSpinner from '../shared/save_project_loader';
-document.addEventListener('DOMContentLoaded', () => {
- initFilePickers();
- initConfirmDangerModal();
- initSettingsPanels();
- initProjectDeleteButton();
- mountBadgeSettings(PROJECT_BADGE);
+initFilePickers();
+initConfirmDangerModal();
+initSettingsPanels();
+initProjectDeleteButton();
+mountBadgeSettings(PROJECT_BADGE);
- new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new
- initServiceDesk();
+new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new
+initServiceDesk();
- initProjectLoadingSpinner();
- initProjectPermissionsSettings();
- setupTransferEdit('.js-project-transfer-form', 'select.select2');
+initProjectLoadingSpinner();
+initProjectPermissionsSettings();
+setupTransferEdit('.js-project-transfer-form', 'select.select2');
- dirtySubmitFactory(
- document.querySelectorAll(
- '.js-general-settings-form, .js-mr-settings-form, .js-mr-approvals-form',
- ),
- );
+dirtySubmitFactory(
+ document.querySelectorAll(
+ '.js-general-settings-form, .js-mr-settings-form, .js-mr-approvals-form',
+ ),
+);
- mountSearchSettings();
-});
+initSearchSettings();
diff --git a/app/assets/javascripts/pages/projects/edit/mount_search_settings.js b/app/assets/javascripts/pages/projects/edit/mount_search_settings.js
deleted file mode 100644
index 6c477dd7e80..00000000000
--- a/app/assets/javascripts/pages/projects/edit/mount_search_settings.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const mountSearchSettings = async () => {
- const el = document.querySelector('.js-search-settings-app');
-
- if (el) {
- const { default: initSearch } = await import(
- /* webpackChunkName: 'search_settings' */ '~/search_settings'
- );
- initSearch({ el });
- }
-};
-
-export default mountSearchSettings;
diff --git a/app/assets/javascripts/pages/projects/environments/index/index.js b/app/assets/javascripts/pages/projects/environments/index/index.js
index 4d5106f6d5f..554ed4f9786 100644
--- a/app/assets/javascripts/pages/projects/environments/index/index.js
+++ b/app/assets/javascripts/pages/projects/environments/index/index.js
@@ -1,3 +1,3 @@
import initEnvironments from '~/environments/';
-document.addEventListener('DOMContentLoaded', initEnvironments);
+initEnvironments();
diff --git a/app/assets/javascripts/pages/projects/environments/show/index.js b/app/assets/javascripts/pages/projects/environments/show/index.js
index 5d3a153cbd1..a4960037eaa 100644
--- a/app/assets/javascripts/pages/projects/environments/show/index.js
+++ b/app/assets/javascripts/pages/projects/environments/show/index.js
@@ -1,3 +1,3 @@
import initShowEnvironment from '~/environments/mount_show';
-document.addEventListener('DOMContentLoaded', initShowEnvironment);
+initShowEnvironment();
diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js
index 388d7d7bdda..a8225167c6b 100644
--- a/app/assets/javascripts/pages/projects/find_file/show/index.js
+++ b/app/assets/javascripts/pages/projects/find_file/show/index.js
@@ -1,13 +1,11 @@
import $ from 'jquery';
-import ProjectFindFile from '~/project_find_file';
import ShortcutsFindFile from '~/behaviors/shortcuts/shortcuts_find_file';
+import ProjectFindFile from '~/project_find_file';
-document.addEventListener('DOMContentLoaded', () => {
- const findElement = document.querySelector('.js-file-finder');
- const projectFindFile = new ProjectFindFile($('.file-finder-holder'), {
- url: findElement.dataset.fileFindUrl,
- treeUrl: findElement.dataset.findTreeUrl,
- blobUrlTemplate: findElement.dataset.blobUrlTemplate,
- });
- new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new
+const findElement = document.querySelector('.js-file-finder');
+const projectFindFile = new ProjectFindFile($('.file-finder-holder'), {
+ url: findElement.dataset.fileFindUrl,
+ treeUrl: findElement.dataset.findTreeUrl,
+ blobUrlTemplate: findElement.dataset.blobUrlTemplate,
});
+new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
index a614443bcd9..bc47b124f8b 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
@@ -1,8 +1,8 @@
<script>
import { GlTabs, GlTab, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import ForkGroupsListItem from './fork_groups_list_item.vue';
export default {
@@ -14,10 +14,6 @@ export default {
ForkGroupsListItem,
},
props: {
- hasReachedProjectLimit: {
- type: Boolean,
- required: true,
- },
endpoint: {
type: String,
required: true,
@@ -77,7 +73,6 @@ export default {
v-for="(namespace, index) in filteredNamespaces"
:key="index"
:group="namespace"
- :has-reached-project-limit="hasReachedProjectLimit"
/>
</ul>
</gl-tab>
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
index 57838050d55..46d1696b88b 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
@@ -10,7 +10,6 @@ import {
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/groups/constants';
-import { __ } from '~/locale';
import csrf from '~/lib/utils/csrf';
export default {
@@ -31,10 +30,6 @@ export default {
type: Object,
required: true,
},
- hasReachedProjectLimit: {
- type: Boolean,
- required: true,
- },
},
data() {
return { namespaces: null, isForking: false };
@@ -60,12 +55,7 @@ export default {
return GROUP_VISIBILITY_TYPE[this.group.visibility];
},
isSelectButtonDisabled() {
- return this.hasReachedProjectLimit || !this.group.can_create_project;
- },
- selectButtonDisabledTooltip() {
- return this.hasReachedProjectLimit
- ? this.$options.i18n.hasReachedProjectLimitMessage
- : this.$options.i18n.insufficientPermissionsMessage;
+ return !this.group.can_create_project;
},
},
@@ -76,13 +66,6 @@ export default {
},
},
- i18n: {
- hasReachedProjectLimitMessage: __('You have reached your project limit'),
- insufficientPermissionsMessage: __(
- 'You must have permission to create a project in a namespace before forking.',
- ),
- },
-
csrf,
};
</script>
@@ -94,7 +77,7 @@ export default {
</div>
<gl-link
:href="group.relative_path"
- class="gl-display-none gl-flex-shrink-0 gl-display-sm-flex gl-mr-3"
+ class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3"
>
<gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatarUrl" />
</gl-link>
@@ -113,7 +96,7 @@ export default {
<gl-badge
v-if="isGroupPendingRemoval"
variant="warning"
- class="gl-display-none gl-display-sm-flex gl-mt-3 gl-mr-1"
+ class="gl-display-none gl-sm-display-flex gl-mt-3 gl-mr-1"
>{{ __('pending removal') }}</gl-badge
>
<span v-if="group.permission" class="user-access-role gl-mt-3">
@@ -149,7 +132,9 @@ export default {
</form>
</div>
<gl-tooltip v-if="isSelectButtonDisabled" :target="() => $refs.selectButtonWrapper">
- {{ selectButtonDisabledTooltip }}
+ {{
+ __('You must have permission to create a project in a namespace before forking.')
+ }}
</gl-tooltip>
</template>
</div>
diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js
index 79485859738..a018d7e0926 100644
--- a/app/assets/javascripts/pages/projects/forks/new/index.js
+++ b/app/assets/javascripts/pages/projects/forks/new/index.js
@@ -1,13 +1,10 @@
import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
import ForkGroupsList from './components/fork_groups_list.vue';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('fork-groups-mount-element');
- const { endpoint, canCreateProject } = mountElement.dataset;
-
- const hasReachedProjectLimit = !parseBoolean(canCreateProject);
+ const { endpoint } = mountElement.dataset;
return new Vue({
el: mountElement,
@@ -15,7 +12,6 @@ document.addEventListener('DOMContentLoaded', () => {
return h(ForkGroupsList, {
props: {
endpoint,
- hasReachedProjectLimit,
},
});
},
diff --git a/app/assets/javascripts/pages/projects/incidents/show/index.js b/app/assets/javascripts/pages/projects/incidents/show/index.js
index 5b3f03cd57e..a75b68873ef 100644
--- a/app/assets/javascripts/pages/projects/incidents/show/index.js
+++ b/app/assets/javascripts/pages/projects/incidents/show/index.js
@@ -1,9 +1,7 @@
-import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initRelatedIssues from '~/related_issues';
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initShow from '../../issues/show';
initShow();
-if (!gon.features?.vueIssuableSidebar) {
- initSidebarBundle();
-}
+initSidebarBundle();
initRelatedIssues();
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 3e9962a4e72..45e9643b3f3 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,5 +1,5 @@
-import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
+import Project from './project';
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index 5eb0d323266..06aba866ccf 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -1,9 +1,9 @@
-import LineHighlighter from '~/line_highlighter';
-import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
-import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import BlobForkSuggestion from '~/blob/blob_fork_suggestion';
+import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
import initBlobBundle from '~/blob_edit/blob_bundle';
+import LineHighlighter from '~/line_highlighter';
export default () => {
new LineHighlighter(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js
index 9f20a3e4e46..764c23e9a99 100644
--- a/app/assets/javascripts/pages/projects/init_form.js
+++ b/app/assets/javascripts/pages/projects/init_form.js
@@ -1,7 +1,7 @@
-import ZenMode from '~/zen_mode';
import GLForm from '~/gl_form';
+import ZenMode from '~/zen_mode';
-export default function ($formEl) {
+export default function initProjectForm($formEl) {
new ZenMode(); // eslint-disable-line no-new
new GLForm($formEl); // eslint-disable-line no-new
}
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js
index 34c7ee2e603..4e35f28ab06 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/pages/projects/issues/form.js
@@ -2,12 +2,12 @@
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable_form';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
+import initSuggestions from '~/issuable_suggestions';
import LabelsSelect from '~/labels_select';
import MilestoneSelect from '~/milestone_select';
-import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
-import initSuggestions from '~/issuable_suggestions';
export default () => {
new ShortcutsNavigation();
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index f3ccedc47c8..525d90e162d 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -1,15 +1,15 @@
/* eslint-disable no-new */
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
-import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import UsersSelect from '~/users_select';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
-import { FILTERED_SEARCH } from '~/pages/constants';
-import { ISSUABLE_INDEX } from '~/pages/projects/constants';
+import initIssuableByEmail from '~/issuable/init_issuable_by_email';
+import IssuableIndex from '~/issuable_index';
import initIssuablesList from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
-import { showLearnGitLabIssuesPopover } from '~/onboarding_issues';
+import { FILTERED_SEARCH } from '~/pages/constants';
+import { ISSUABLE_INDEX } from '~/pages/projects/constants';
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import UsersSelect from '~/users_select';
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
@@ -25,4 +25,4 @@ new UsersSelect();
initManualOrdering();
initIssuablesList();
-showLearnGitLabIssuesPopover();
+initIssuableByEmail();
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js
index ccb453a59ea..bec207aa439 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js
@@ -1,6 +1,6 @@
/* eslint-disable class-methods-use-this */
-import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
const AUTHOR_PARAM_KEY = 'author_username';
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
index 231ee6732e9..5be9f6117dc 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,5 +1,5 @@
-import FilteredSearchServiceDesk from './filtered_search';
import initIssuablesList from '~/issues_list';
+import FilteredSearchServiceDesk from './filtered_search';
const supportBotData = JSON.parse(
document.querySelector('.js-service-desk-issues').dataset.supportBot,
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 7068574ecb8..992bf3c54ff 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -1,22 +1,21 @@
import loadAwardsHandler from '~/awards_handler';
+import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
+import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
+import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
+import { IssuableType } from '~/issuable_show/constants';
import Issue from '~/issue';
-import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
-import ZenMode from '~/zen_mode';
import '~/notes/index';
-import { store } from '~/notes/stores';
-import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue';
import initIncidentApp from '~/issue_show/incident';
-import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
-import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
-import initRelatedMergeRequestsApp from '~/related_merge_requests';
+import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue';
import { parseIssuableData } from '~/issue_show/utils/parse_data';
-import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
-import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
-
-import { IssuableType } from '~/issuable_show/constants';
+import { store } from '~/notes/stores';
+import initRelatedMergeRequestsApp from '~/related_merge_requests';
+import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
+import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
+import ZenMode from '~/zen_mode';
-export default function () {
+export default function initShowIssue() {
const initialDataEl = document.getElementById('js-issuable-app');
const { issueType, ...issuableData } = parseIssuableData(initialDataEl);
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index 630add51a97..e4f99d1e7fd 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,9 +1,7 @@
-import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initRelatedIssues from '~/related_issues';
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initShow from '../show';
initShow();
-if (gon.features && !gon.features.vueIssuableSidebar) {
- initSidebarBundle();
-}
+initSidebarBundle();
initRelatedIssues();
diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js
index c343a37b292..6a70d4cf26d 100644
--- a/app/assets/javascripts/pages/projects/jobs/index/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/index/index.js
@@ -1,26 +1,19 @@
import Vue from 'vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-import Tracking from '~/tracking';
document.addEventListener('DOMContentLoaded', () => {
const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
remainingTimeElements.forEach(
(el) =>
new Vue({
- ...GlCountdown,
el,
- propsData: {
- endDateString: el.dateTime,
+ render(h) {
+ return h(GlCountdown, {
+ props: {
+ endDateString: el.dateTime,
+ },
+ });
},
}),
);
-
- const trackButtonClick = () => {
- if (gon.tracking_data) {
- const { category, action, ...data } = gon.tracking_data;
- Tracking.event(category, action, data);
- }
- };
- const buttons = document.querySelectorAll('.js-empty-state-button');
- buttons.forEach((button) => button.addEventListener('click', trackButtonClick));
});
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 8626fd18233..81ffaa6f7a3 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -1,9 +1,9 @@
<script>
import { GlSprintf, GlModal } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__, __, sprintf } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
+import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 4f5e5c8cceb..9f782c07101 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -1,8 +1,9 @@
import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
import initLabels from '~/init_labels';
-import eventHub from '../event_hub';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
+import Translate from '~/vue_shared/translate';
import PromoteLabelModal from '../components/promote_label_modal.vue';
+import eventHub from '../event_hub';
Vue.use(Translate);
@@ -49,7 +50,7 @@ const initLabelIndex = () => {
promoteLabelButtons.forEach((button) => {
button.removeAttribute('disabled');
button.addEventListener('click', () => {
- this.$root.$emit('bv::show::modal', 'promote-label-modal');
+ this.$root.$emit(BV_SHOW_MODAL, 'promote-label-modal');
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
this.setModalProps({
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
new file mode 100644
index 00000000000..0393793bfe1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
@@ -0,0 +1,27 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { ACTION_TEXT } from '../constants';
+
+export default {
+ components: { GlLink },
+ i18n: {
+ ACTION_TEXT,
+ },
+ props: {
+ actions: {
+ required: true,
+ type: Object,
+ },
+ },
+};
+</script>
+<template>
+ <ul>
+ <li v-for="(value, action) in actions" :key="action">
+ <span v-if="value.completed">{{ $options.i18n.ACTION_TEXT[action] }}</span>
+ <span v-else>
+ <gl-link :href="value.url">{{ $options.i18n.ACTION_TEXT[action] }}</gl-link>
+ </span>
+ </li>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
new file mode 100644
index 00000000000..0393793bfe1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
@@ -0,0 +1,27 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { ACTION_TEXT } from '../constants';
+
+export default {
+ components: { GlLink },
+ i18n: {
+ ACTION_TEXT,
+ },
+ props: {
+ actions: {
+ required: true,
+ type: Object,
+ },
+ },
+};
+</script>
+<template>
+ <ul>
+ <li v-for="(value, action) in actions" :key="action">
+ <span v-if="value.completed">{{ $options.i18n.ACTION_TEXT[action] }}</span>
+ <span v-else>
+ <gl-link :href="value.url">{{ $options.i18n.ACTION_TEXT[action] }}</gl-link>
+ </span>
+ </li>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
new file mode 100644
index 00000000000..8606af29785
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
@@ -0,0 +1,12 @@
+import { s__ } from '~/locale';
+
+export const ACTION_TEXT = {
+ gitWrite: s__('LearnGitLab|Create a repository'),
+ userAdded: s__('LearnGitLab|Invite your colleagues'),
+ pipelineCreated: s__('LearnGitLab|Set-up CI/CD'),
+ trialStarted: s__('LearnGitLab|Start a free trial of GitLab Gold'),
+ codeOwnersEnabled: s__('LearnGitLab|Add code owners'),
+ requiredMrApprovalsEnabled: s__('LearnGitLab|Enable require merge approvals'),
+ mergeRequestCreated: s__('LearnGitLab|Submit a merge request (MR)'),
+ securityScanEnabled: s__('LearnGitLab|Run a Security scan using CI/CD'),
+};
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
new file mode 100644
index 00000000000..c4dec89b984
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import LearnGitlabA from '../components/learn_gitlab_a.vue';
+import LearnGitlabB from '../components/learn_gitlab_b.vue';
+
+function initLearnGitlab() {
+ const el = document.getElementById('js-learn-gitlab-app');
+
+ if (!el) {
+ return false;
+ }
+
+ const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
+
+ const { learnGitlabA } = gon.experiments;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } });
+ },
+ });
+}
+
+initLearnGitlab();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
index 28641104c58..05019915fc9 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
@@ -1,5 +1,5 @@
-import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initMergeConflicts from '~/merge_conflicts/merge_conflicts_bundle';
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
document.addEventListener('DOMContentLoaded', () => {
initSidebarBundle();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/index.js
index febfecebbd2..34d9fa03d24 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/index.js
@@ -1,3 +1,3 @@
import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request';
-document.addEventListener('DOMContentLoaded', initMergeRequest);
+initMergeRequest();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
index eb2692c7cb4..1a0fa6e544e 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import { localTimeAgo } from '~/lib/utils/datetime_utility';
-import axios from '~/lib/utils/axios_utils';
import initCompareAutocomplete from '~/compare_autocomplete';
+import axios from '~/lib/utils/axios_utils';
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
import initTargetProjectDropdown from './target_project_dropdown';
const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index 01a0b4870c1..9aecd154483 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -1,17 +1,15 @@
-import MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import MergeRequest from '~/merge_request';
import initCompare from './compare';
-document.addEventListener('DOMContentLoaded', () => {
- const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
- if (mrNewCompareNode) {
- initCompare(mrNewCompareNode);
- } else {
- const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
- // eslint-disable-next-line no-new
- new MergeRequest({
- action: mrNewSubmitNode.dataset.mrSubmitAction,
- });
- initPipelines();
- }
-});
+const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
+if (mrNewCompareNode) {
+ initCompare(mrNewCompareNode);
+} else {
+ const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
+ // eslint-disable-next-line no-new
+ new MergeRequest({
+ action: mrNewSubmitNode.dataset.mrSubmitAction,
+ });
+ initPipelines();
+}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/check_form_state.js b/app/assets/javascripts/pages/projects/merge_requests/edit/check_form_state.js
new file mode 100644
index 00000000000..74178ab96e3
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/edit/check_form_state.js
@@ -0,0 +1,24 @@
+import { serializeForm } from '~/lib/utils/forms';
+
+const findForm = () => document.querySelector('.merge-request-form');
+const serializeFormData = () => JSON.stringify(serializeForm(findForm()));
+
+export default () => {
+ const oldFormData = serializeFormData();
+
+ const compareFormData = (e) => {
+ const newFormData = serializeFormData();
+
+ if (oldFormData !== newFormData) {
+ e.preventDefault();
+ // eslint-disable-next-line no-param-reassign
+ e.returnValue = ''; // Chrome requires returnValue to be set
+ }
+ };
+
+ window.addEventListener('beforeunload', compareFormData);
+
+ findForm().addEventListener('submit', () =>
+ window.removeEventListener('beforeunload', compareFormData),
+ );
+};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
index febfecebbd2..399aebb0c83 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
@@ -1,3 +1,7 @@
import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request';
+import initCheckFormState from './check_form_state';
-document.addEventListener('DOMContentLoaded', initMergeRequest);
+document.addEventListener('DOMContentLoaded', () => {
+ initMergeRequest();
+ initCheckFormState();
+});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index 94a12cc2706..76705256fe2 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -1,11 +1,12 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
-import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import UsersSelect from '~/users_select';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import initIssuableByEmail from '~/issuable/init_issuable_by_email';
+import IssuableIndex from '~/issuable_index';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
+import initFilteredSearch from '~/pages/search/init_filtered_search';
+import UsersSelect from '~/users_select';
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
@@ -19,3 +20,5 @@ initFilteredSearch({
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
+
+initIssuableByEmail();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index 76d72efb11b..7d5719cf8a8 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -2,8 +2,8 @@
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable_form';
-import Diff from '~/diff';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
+import Diff from '~/diff';
import GLForm from '~/gl_form';
import LabelsSelect from '~/labels_select';
import MilestoneSelect from '~/milestone_select';
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 1a0c5860991..d4d5e9f2711 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -1,16 +1,16 @@
import Vue from 'vue';
-import ZenMode from '~/zen_mode';
-import initIssuableSidebar from '~/init_issuable_sidebar';
+import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
-import { handleLocationHash } from '~/lib/utils/common_utils';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
-import initSourcegraph from '~/sourcegraph';
-import loadAwardsHandler from '~/awards_handler';
-import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
+import initIssuableSidebar from '~/init_issuable_sidebar';
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
+import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
+import { handleLocationHash } from '~/lib/utils/common_utils';
import StatusBox from '~/merge_request/components/status_box.vue';
+import initSourcegraph from '~/sourcegraph';
+import ZenMode from '~/zen_mode';
-export default function () {
+export default function initMergeRequestShow() {
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initPipelines();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index 602d749ee07..546fa66eda6 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,14 +1,12 @@
-import initMrNotes from '~/mr_notes';
import { initReviewBar } from '~/batch_comments';
+import initMrNotes from '~/mr_notes';
+import store from '~/mr_notes/stores';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initShow from '../init_merge_request_show';
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
-import store from '~/mr_notes/stores';
+import initShow from '../init_merge_request_show';
initShow();
-if (gon.features && !gon.features.vueIssuableSidebar) {
- initSidebarBundle();
-}
+initSidebarBundle();
initMrNotes();
initReviewBar();
initIssuableHeaderWarning(store);
diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js
index 84a52421598..a853413e1f7 100644
--- a/app/assets/javascripts/pages/projects/milestones/show/index.js
+++ b/app/assets/javascripts/pages/projects/milestones/show/index.js
@@ -1,5 +1,5 @@
-import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
import milestones from '~/pages/milestones/shared';
+import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
document.addEventListener('DOMContentLoaded', () => {
initMilestonesShow();
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index 88f4db3ec08..437594fdf11 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -1,7 +1,7 @@
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { __ } from '~/locale';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
-import { __ } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
document.addEventListener('DOMContentLoaded', () => {
initProjectVisibilitySelector();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index aa7414f3ae7..3b19231720a 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -1,7 +1,7 @@
<script>
import { GlFormRadio, GlFormRadioGroup, GlLink, GlSprintf } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
import { getWeekdayNames } from '~/lib/utils/datetime_utility';
+import { s__, sprintf } from '~/locale';
const KEY_EVERY_DAY = 'everyDay';
const KEY_EVERY_WEEK = 'everyWeek';
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index 8ee9d481466..92b2bc9644b 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -1,9 +1,9 @@
<script>
-import Vue from 'vue';
-import Cookies from 'js-cookie';
import { GlButton } from '@gitlab/ui';
-import Translate from '../../../../../vue_shared/translate';
+import Cookies from 'js-cookie';
+import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import Translate from '../../../../../vue_shared/translate';
Vue.use(Translate);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
index 497e2c9c0ae..ce0e573fed2 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import Vue from 'vue';
-import Translate from '../../../../vue_shared/translate';
+import setupNativeFormVariableList from '../../../../ci_variable_list/native_form_variable_list';
import GlFieldErrors from '../../../../gl_field_errors';
+import Translate from '../../../../vue_shared/translate';
import intervalPatternInput from './components/interval_pattern_input.vue';
-import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
-import setupNativeFormVariableList from '../../../../ci_variable_list/native_form_variable_list';
+import TimezoneDropdown from './components/timezone_dropdown';
Vue.use(Translate);
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
index 08c31f2b3c6..32299287a9c 100644
--- a/app/assets/javascripts/pages/projects/pipelines/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import NewBranchForm from '~/new_branch_form';
import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
+import NewBranchForm from '~/new_branch_form';
import initNewPipeline from '~/pipeline_new/index';
const el = document.getElementById('js-new-pipeline');
diff --git a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js b/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
index 0539d318471..ba03fccdb03 100644
--- a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
+++ b/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
@@ -1,3 +1,3 @@
import initActivityCharts from '~/analytics/product_analytics/activity_charts_bundle';
-document.addEventListener('DOMContentLoaded', () => initActivityCharts());
+initActivityCharts();
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index ef6953db83b..da8dc527d79 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -2,14 +2,14 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
-import { __ } from '~/locale';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { serializeForm } from '~/lib/utils/forms';
-import axios from '~/lib/utils/axios_utils';
+import initClonePanel from '~/clone_panel';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { serializeForm } from '~/lib/utils/forms';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import projectSelect from '../../project_select';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import initClonePanel from '~/clone_panel';
export default class Project {
constructor() {
@@ -126,8 +126,9 @@ export default class Project {
const refs = this.fullData.Branches.concat(this.fullData.Tags);
const currentRef = refs.find((ref) => loc.indexOf(ref) > -1);
if (currentRef) {
- const targetPath = loc.split(currentRef)[1].slice(1);
+ const targetPath = loc.split(currentRef)[1].slice(1).split('#')[0];
selectedUrl.searchParams.set('path', targetPath);
+ selectedUrl.hash = window.location.hash;
}
}
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 3e0a48ee6a2..ed11b07be4a 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -1,11 +1,13 @@
import Vue from 'vue';
-import Members from '~/members';
-import memberExpirationDate from '~/member_expiration_date';
-import UsersSelect from '~/users_select';
+import { deprecatedCreateFlash as flash } from '~/flash';
import groupsSelect from '~/groups_select';
-import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
+import { __ } from '~/locale';
+import memberExpirationDate from '~/member_expiration_date';
+import Members from '~/members';
+import UsersSelect from '~/users_select';
+import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
@@ -21,14 +23,74 @@ function mountRemoveMemberModal() {
});
}
-document.addEventListener('DOMContentLoaded', () => {
- groupsSelect();
- memberExpirationDate();
- memberExpirationDate('.js-access-expiration-date-groups');
- mountRemoveMemberModal();
- initInviteMembersModal();
- initInviteMembersTrigger();
-
- new Members(); // eslint-disable-line no-new
- new UsersSelect(); // eslint-disable-line no-new
-});
+groupsSelect();
+memberExpirationDate();
+memberExpirationDate('.js-access-expiration-date-groups');
+mountRemoveMemberModal();
+initInviteMembersModal();
+initInviteMembersTrigger();
+
+new Members(); // eslint-disable-line no-new
+new UsersSelect(); // eslint-disable-line no-new
+
+if (window.gon.features.vueProjectMembersList) {
+ const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
+
+ Promise.all([
+ import('~/members/index'),
+ import('~/members/utils'),
+ import('~/projects/members/utils'),
+ import('~/locale'),
+ ])
+ .then(
+ ([
+ { initMembersApp },
+ { groupLinkRequestFormatter },
+ { projectMemberRequestFormatter },
+ { s__ },
+ ]) => {
+ initMembersApp(document.querySelector('.js-project-members-list'), {
+ tableFields: SHARED_FIELDS.concat(['source', 'granted']),
+ tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
+ tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
+ requestFormatter: projectMemberRequestFormatter,
+ filteredSearchBar: {
+ show: true,
+ tokens: ['with_inherited_permissions'],
+ searchParam: 'search',
+ placeholder: s__('Members|Filter members'),
+ recentSearchesStorageKey: 'project_members',
+ },
+ });
+
+ initMembersApp(document.querySelector('.js-project-group-links-list'), {
+ tableFields: SHARED_FIELDS.concat('granted'),
+ tableAttrs: {
+ table: { 'data-qa-selector': 'groups_list' },
+ tr: { 'data-qa-selector': 'group_row' },
+ },
+ requestFormatter: groupLinkRequestFormatter,
+ filteredSearchBar: {
+ show: true,
+ tokens: [],
+ searchParam: 'search_groups',
+ placeholder: s__('Members|Search groups'),
+ recentSearchesStorageKey: 'project_group_links',
+ },
+ });
+
+ initMembersApp(document.querySelector('.js-project-invited-members-list'), {
+ tableFields: SHARED_FIELDS.concat('invited'),
+ requestFormatter: projectMemberRequestFormatter,
+ });
+
+ initMembersApp(document.querySelector('.js-project-access-requests-list'), {
+ tableFields: SHARED_FIELDS.concat('requested'),
+ requestFormatter: projectMemberRequestFormatter,
+ });
+ },
+ )
+ .catch(() => {
+ flash(__('An error occurred while loading the members, please try again.'));
+ });
+}
diff --git a/app/assets/javascripts/pages/projects/releases/edit/index.js b/app/assets/javascripts/pages/projects/releases/edit/index.js
index efa059dcd6d..e4a8b71eb16 100644
--- a/app/assets/javascripts/pages/projects/releases/edit/index.js
+++ b/app/assets/javascripts/pages/projects/releases/edit/index.js
@@ -1,7 +1,5 @@
-import ZenMode from '~/zen_mode';
import initEditRelease from '~/releases/mount_edit';
+import ZenMode from '~/zen_mode';
-document.addEventListener('DOMContentLoaded', () => {
- new ZenMode(); // eslint-disable-line no-new
- initEditRelease();
-});
+new ZenMode(); // eslint-disable-line no-new
+initEditRelease();
diff --git a/app/assets/javascripts/pages/projects/releases/index/index.js b/app/assets/javascripts/pages/projects/releases/index/index.js
index 24c9cd528b3..caf95ae53c8 100644
--- a/app/assets/javascripts/pages/projects/releases/index/index.js
+++ b/app/assets/javascripts/pages/projects/releases/index/index.js
@@ -1,3 +1,3 @@
import initReleases from '~/releases/mount_index';
-document.addEventListener('DOMContentLoaded', initReleases);
+initReleases();
diff --git a/app/assets/javascripts/pages/projects/releases/new/index.js b/app/assets/javascripts/pages/projects/releases/new/index.js
index 0e314aacf8a..31c1715ce2a 100644
--- a/app/assets/javascripts/pages/projects/releases/new/index.js
+++ b/app/assets/javascripts/pages/projects/releases/new/index.js
@@ -1,7 +1,5 @@
-import ZenMode from '~/zen_mode';
import initNewRelease from '~/releases/mount_new';
+import ZenMode from '~/zen_mode';
-document.addEventListener('DOMContentLoaded', () => {
- new ZenMode(); // eslint-disable-line no-new
- initNewRelease();
-});
+new ZenMode(); // eslint-disable-line no-new
+initNewRelease();
diff --git a/app/assets/javascripts/pages/projects/releases/show/index.js b/app/assets/javascripts/pages/projects/releases/show/index.js
index 4e17e6ff311..0ec70ef24b6 100644
--- a/app/assets/javascripts/pages/projects/releases/show/index.js
+++ b/app/assets/javascripts/pages/projects/releases/show/index.js
@@ -1,3 +1,3 @@
import initShowRelease from '~/releases/mount_show';
-document.addEventListener('DOMContentLoaded', initShowRelease);
+initShowRelease();
diff --git a/app/assets/javascripts/pages/projects/security/configuration/index.js b/app/assets/javascripts/pages/projects/security/configuration/index.js
new file mode 100644
index 00000000000..101cb8356b2
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/security/configuration/index.js
@@ -0,0 +1,3 @@
+import { initStaticSecurityConfiguration } from '~/security_configuration';
+
+initStaticSecurityConfiguration(document.querySelector('#js-security-configuration-static'));
diff --git a/app/assets/javascripts/pages/projects/serverless/index.js b/app/assets/javascripts/pages/projects/serverless/index.js
index a883737ac9b..640301dd478 100644
--- a/app/assets/javascripts/pages/projects/serverless/index.js
+++ b/app/assets/javascripts/pages/projects/serverless/index.js
@@ -1,7 +1,5 @@
import ServerlessBundle from '~/serverless/serverless_bundle';
import initServerlessSurveyBanner from '~/serverless/survey_banner';
-document.addEventListener('DOMContentLoaded', () => {
- initServerlessSurveyBanner();
- new ServerlessBundle(); // eslint-disable-line no-new
-});
+initServerlessSurveyBanner();
+new ServerlessBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js
index 04f3877ab48..8e603c5c1a2 100644
--- a/app/assets/javascripts/pages/projects/services/edit/index.js
+++ b/app/assets/javascripts/pages/projects/services/edit/index.js
@@ -1,7 +1,6 @@
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
-import CustomMetrics from '~/prometheus_metrics/custom_metrics';
import PrometheusAlerts from '~/prometheus_alerts';
-import initAlertsSettings from '~/alerts_service_settings';
+import CustomMetrics from '~/prometheus_metrics/custom_metrics';
document.addEventListener('DOMContentLoaded', () => {
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
@@ -15,5 +14,4 @@ document.addEventListener('DOMContentLoaded', () => {
}
PrometheusAlerts();
- initAlertsSettings(document.querySelector('.js-alerts-service-settings'));
});
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 1321155b7ec..be9259ec3ca 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -1,11 +1,12 @@
-import initSettingsPanels from '~/settings_panels';
+import initArtifactsSettings from '~/artifacts_settings';
import SecretValues from '~/behaviors/secret_values';
-import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
+import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
import initVariableList from '~/ci_variable_list';
import initDeployFreeze from '~/deploy_freeze';
-import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
+import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
-import initArtifactsSettings from '~/artifacts_settings';
+import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
+import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
@@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (gon?.features?.vueifySharedRunnersToggle) {
initSharedRunnersToggle();
}
+
+ initInstallRunner();
});
diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
index 153ccffd472..3a46241e2eb 100644
--- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
@@ -1,9 +1,9 @@
-import mountErrorTrackingForm from '~/error_tracking_settings';
import mountAlertsSettings from '~/alerts_settings';
-import mountOperationSettings from '~/operation_settings';
+import mountErrorTrackingForm from '~/error_tracking_settings';
import mountGrafanaIntegration from '~/grafana_integration';
-import initSettingsPanels from '~/settings_panels';
import initIncidentsSettings from '~/incidents_settings';
+import mountOperationSettings from '~/operation_settings';
+import initSettingsPanels from '~/settings_panels';
initIncidentsSettings();
mountErrorTrackingForm();
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
index eff45bad603..8d390c8586b 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/form.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -1,13 +1,13 @@
/* eslint-disable no-new */
-import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
-import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
-import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
-import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
-import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
import DueDateSelectors from '~/due_date_select';
import fileUpload from '~/lib/utils/file_upload';
+import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
+import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
+import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
+import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
+import initSettingsPanels from '~/settings_panels';
export default () => {
new ProtectedTagCreate();
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index 1ef4b460263..e90954c14c5 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,5 +1,5 @@
-import initForm from '../form';
import MirrorRepos from '~/mirrors/mirror_repos';
+import initForm from '../form';
document.addEventListener('DOMContentLoaded', () => {
initForm();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
index eee666bea05..d62df77ad2c 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
@@ -1,12 +1,11 @@
<script>
-import { GlIcon } from '@gitlab/ui';
-import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
+import { GlIcon, GlToggle } from '@gitlab/ui';
import { featureAccessLevelNone } from '../constants';
export default {
components: {
GlIcon,
- projectFeatureToggle,
+ GlToggle,
},
model: {
prop: 'value',
@@ -78,11 +77,11 @@ export default {
class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0"
>
<input v-if="name" :name="name" :value="value" type="hidden" />
- <project-feature-toggle
+ <gl-toggle
v-if="showToggle"
- class="gl-flex-grow-0 gl-mr-3"
+ class="gl-mr-3"
:value="featureEnabled"
- :disabled-input="disabledInput"
+ :disabled="disabledInput"
@change="toggleFeature"
/>
<div class="select-wrapper gl-flex-fill-1">
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 4af476fbd68..94a9bc168e5 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,11 +1,9 @@
<script>
-import { GlIcon, GlSprintf, GlLink, GlFormCheckbox } from '@gitlab/ui';
+import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
import { s__ } from '~/locale';
-import projectFeatureSetting from './project_feature_setting.vue';
-import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
-import projectSettingRow from './project_setting_row.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
visibilityOptions,
visibilityLevelDescriptions,
@@ -15,19 +13,20 @@ import {
featureAccessLevelNone,
} from '../constants';
import { toggleHiddenClassBySelector } from '../external';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import projectFeatureSetting from './project_feature_setting.vue';
+import projectSettingRow from './project_setting_row.vue';
const PAGE_FEATURE_ACCESS_LEVEL = s__('ProjectSettings|Everyone');
export default {
components: {
projectFeatureSetting,
- projectFeatureToggle,
projectSettingRow,
GlIcon,
GlSprintf,
GlLink,
GlFormCheckbox,
+ GlToggle,
},
mixins: [settingsMixin, glFeatureFlagsMixin()],
@@ -75,6 +74,11 @@ export default {
required: false,
default: false,
},
+ securityAndComplianceAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
visibilityHelpPath: {
type: String,
required: false,
@@ -141,6 +145,7 @@ export default {
metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
analyticsAccessLevel: featureAccessLevel.EVERYONE,
requirementsAccessLevel: featureAccessLevel.EVERYONE,
+ securityAndComplianceAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
operationsAccessLevel: featureAccessLevel.EVERYONE,
containerRegistryEnabled: true,
lfsEnabled: true,
@@ -218,11 +223,11 @@ export default {
repositoryHelpText() {
if (this.visibilityLevel === visibilityOptions.PRIVATE) {
- return s__('ProjectSettings|View and edit files in this project');
+ return s__('ProjectSettings|View and edit files in this project.');
}
return s__(
- 'ProjectSettings|View and edit files in this project. Non-project members will only have read access',
+ 'ProjectSettings|View and edit files in this project. Non-project members will only have read access.',
);
},
},
@@ -264,6 +269,10 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.requirementsAccessLevel,
);
+ this.securityAndComplianceAccessLevel = Math.min(
+ featureAccessLevel.PROJECT_MEMBERS,
+ this.securityAndComplianceAccessLevel,
+ );
this.operationsAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
this.operationsAccessLevel,
@@ -390,7 +399,7 @@ export default {
name="project[request_access_enabled]"
/>
<input v-model="requestAccessEnabled" type="checkbox" />
- {{ s__('ProjectSettings|Allow users to request access') }}
+ {{ s__('ProjectSettings|Users can request access') }}
</label>
</project-setting-row>
</div>
@@ -401,7 +410,7 @@ export default {
<project-setting-row
ref="issues-settings"
:label="s__('ProjectSettings|Issues')"
- :help-text="s__('ProjectSettings|Lightweight issue tracking system for this project')"
+ :help-text="s__('ProjectSettings|Lightweight issue tracking system.')"
>
<project-feature-setting
v-model="issuesAccessLevel"
@@ -424,7 +433,7 @@ export default {
<project-setting-row
ref="merge-request-settings"
:label="s__('ProjectSettings|Merge requests')"
- :help-text="s__('ProjectSettings|Submit changes to be merged upstream')"
+ :help-text="s__('ProjectSettings|Submit changes to be merged upstream.')"
>
<project-feature-setting
v-model="mergeRequestsAccessLevel"
@@ -436,9 +445,7 @@ export default {
<project-setting-row
ref="fork-settings"
:label="s__('ProjectSettings|Forks')"
- :help-text="
- s__('ProjectSettings|Allow users to make copies of your repository to a new project')
- "
+ :help-text="s__('ProjectSettings|Users can copy the repository to a new project.')"
>
<project-feature-setting
v-model="forkingAccessLevel"
@@ -450,7 +457,7 @@ export default {
<project-setting-row
ref="pipeline-settings"
:label="s__('ProjectSettings|Pipelines')"
- :help-text="s__('ProjectSettings|Build, test, and deploy your changes')"
+ :help-text="s__('ProjectSettings|Build, test, and deploy your changes.')"
>
<project-feature-setting
v-model="buildsAccessLevel"
@@ -475,9 +482,10 @@ export default {
)
}}
</div>
- <project-feature-toggle
+ <gl-toggle
v-model="containerRegistryEnabled"
- :disabled-input="!repositoryEnabled"
+ class="gl-my-2"
+ :disabled="!repositoryEnabled"
name="project[container_registry_enabled]"
/>
</project-setting-row>
@@ -487,19 +495,20 @@ export default {
:help-path="lfsHelpPath"
:label="s__('ProjectSettings|Git Large File Storage (LFS)')"
:help-text="
- s__('ProjectSettings|Manages large files such as audio, video, and graphics files')
+ s__('ProjectSettings|Manages large files such as audio, video, and graphics files.')
"
>
- <project-feature-toggle
+ <gl-toggle
v-model="lfsEnabled"
- :disabled-input="!repositoryEnabled"
+ class="gl-my-2"
+ :disabled="!repositoryEnabled"
name="project[lfs_enabled]"
/>
<p v-if="!lfsEnabled && lfsObjectsExist">
<gl-sprintf
:message="
s__(
- 'ProjectSettings|LFS objects from this repository are still available to forks. %{linkStart}How do I remove them?%{linkEnd}',
+ 'ProjectSettings|LFS objects from this repository are available to forks. %{linkStart}How do I remove them?%{linkEnd}',
)
"
>
@@ -519,12 +528,13 @@ export default {
:help-path="packagesHelpPath"
:label="s__('ProjectSettings|Packages')"
:help-text="
- s__('ProjectSettings|Every project can have its own space to store its packages')
+ s__('ProjectSettings|Every project can have its own space to store its packages.')
"
>
- <project-feature-toggle
+ <gl-toggle
v-model="packagesEnabled"
- :disabled-input="!repositoryEnabled"
+ class="gl-my-2"
+ :disabled="!repositoryEnabled"
name="project[packages_enabled]"
/>
</project-setting-row>
@@ -532,7 +542,7 @@ export default {
<project-setting-row
ref="analytics-settings"
:label="s__('ProjectSettings|Analytics')"
- :help-text="s__('ProjectSettings|View project analytics')"
+ :help-text="s__('ProjectSettings|View project analytics.')"
>
<project-feature-setting
v-model="analyticsAccessLevel"
@@ -544,7 +554,7 @@ export default {
v-if="requirementsAvailable"
ref="requirements-settings"
:label="s__('ProjectSettings|Requirements')"
- :help-text="s__('ProjectSettings|Requirements management system for this project')"
+ :help-text="s__('ProjectSettings|Requirements management system.')"
>
<project-feature-setting
v-model="requirementsAccessLevel"
@@ -553,9 +563,20 @@ export default {
/>
</project-setting-row>
<project-setting-row
+ v-if="securityAndComplianceAvailable"
+ :label="s__('ProjectSettings|Security & Compliance')"
+ :help-text="s__('ProjectSettings|Security & Compliance for this project')"
+ >
+ <project-feature-setting
+ v-model="securityAndComplianceAccessLevel"
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][security_and_compliance_access_level]"
+ />
+ </project-setting-row>
+ <project-setting-row
ref="wiki-settings"
:label="s__('ProjectSettings|Wiki')"
- :help-text="s__('ProjectSettings|Pages for project documentation')"
+ :help-text="s__('ProjectSettings|Pages for project documentation.')"
>
<project-feature-setting
v-model="wikiAccessLevel"
@@ -566,7 +587,7 @@ export default {
<project-setting-row
ref="snippet-settings"
:label="s__('ProjectSettings|Snippets')"
- :help-text="s__('ProjectSettings|Share code pastes with others out of Git repository')"
+ :help-text="s__('ProjectSettings|Share code with others outside the project.')"
>
<project-feature-setting
v-model="snippetsAccessLevel"
@@ -580,7 +601,7 @@ export default {
:help-path="pagesHelpPath"
:label="s__('ProjectSettings|Pages')"
:help-text="
- s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab')
+ s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab.')
"
>
<project-feature-setting
@@ -592,7 +613,7 @@ export default {
<project-setting-row
ref="operations-settings"
:label="s__('ProjectSettings|Operations')"
- :help-text="s__('ProjectSettings|Environments, logs, cluster management, and more')"
+ :help-text="s__('ProjectSettings|Environments, logs, cluster management, and more.')"
>
<project-feature-setting
v-model="operationsAccessLevel"
@@ -604,11 +625,7 @@ export default {
<project-setting-row
ref="metrics-visibility-settings"
:label="__('Metrics Dashboard')"
- :help-text="
- s__(
- 'ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics',
- )
- "
+ :help-text="s__('ProjectSettings|Visualize the project\'s performance metrics.')"
>
<project-feature-setting
v-model="metricsDashboardAccessLevel"
@@ -626,9 +643,7 @@ export default {
{{ s__('ProjectSettings|Disable email notifications') }}
</label>
<span class="form-text text-muted">{{
- s__(
- 'ProjectSettings|This setting will override user notification preferences for all project members.',
- )
+ s__('ProjectSettings|Override user notification preferences for all project members.')
}}</span>
</project-setting-row>
<project-setting-row class="mb-3">
@@ -644,7 +659,7 @@ export default {
{{ s__('ProjectSettings|Show default award emojis') }}
<template #help>{{
s__(
- 'ProjectSettings|When enabled, issues, merge requests, and snippets will always show thumbs-up and thumbs-down award emoji buttons.',
+ 'ProjectSettings|Always show thumbs-up and thumbs-down award emoji buttons on issues, merge requests, and snippets.',
)
}}</template>
</gl-form-checkbox>
@@ -662,9 +677,7 @@ export default {
<gl-form-checkbox v-model="allowEditingCommitMessages">
{{ s__('ProjectSettings|Allow editing commit messages') }}
<template #help>{{
- s__(
- 'ProjectSettings|When enabled, commit authors will be able to edit commit messages on unprotected branches.',
- )
+ s__('ProjectSettings|Commit authors can edit commit messages on unprotected branches.')
}}</template>
</gl-form-checkbox>
</project-setting-row>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js b/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
index ae0936417ad..b52e30dae39 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
@@ -3,6 +3,7 @@ export default {
return {
packagesEnabled: false,
requirementsEnabled: false,
+ securityAndComplianceEnabled: false,
};
},
watch: {
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index cc676b98e49..0494dad6e33 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,17 +1,17 @@
import initTree from 'ee_else_ce/repository';
-import { initUploadForm } from '~/blob_edit/blob_bundle';
+import Activities from '~/activities';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import NotificationsForm from '~/notifications_form';
-import UserCallout from '~/user_callout';
import BlobViewer from '~/blob/viewer/index';
-import Activities from '~/activities';
-import initReadMore from '~/read_more';
+import { initUploadForm } from '~/blob_edit/blob_bundle';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import leaveByUrl from '~/namespaces/leave_by_url';
-import Star from '../../../star';
+import initVueNotificationsDropdown from '~/notifications';
+import NotificationsForm from '~/notifications_form';
+import initReadMore from '~/read_more';
+import UserCallout from '~/user_callout';
import notificationsDropdown from '../../../notifications_dropdown';
-import { showLearnGitLabProjectPopover } from '~/onboarding_issues';
-import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
-import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import Star from '../../../star';
initReadMore();
new Star(); // eslint-disable-line no-new
@@ -40,9 +40,14 @@ if (document.querySelector('.project-show-activity')) {
leaveByUrl('project');
-showLearnGitLabProjectPopover();
+if (gon.features?.vueNotificationDropdown) {
+ initVueNotificationsDropdown();
+} else {
+ notificationsDropdown();
+}
+
+initVueNotificationsDropdown();
-notificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
initInviteMembersTrigger();
diff --git a/app/assets/javascripts/pages/projects/tags/index/index.js b/app/assets/javascripts/pages/projects/tags/index/index.js
index 96e52850936..98560c1193b 100644
--- a/app/assets/javascripts/pages/projects/tags/index/index.js
+++ b/app/assets/javascripts/pages/projects/tags/index/index.js
@@ -1,9 +1,7 @@
import { initRemoveTag } from '../remove_tag';
-document.addEventListener('DOMContentLoaded', () => {
- initRemoveTag({
- onDelete: (path) => {
- document.querySelector(`[data-path="${path}"]`).closest('.js-tag-list').remove();
- },
- });
+initRemoveTag({
+ onDelete: (path) => {
+ document.querySelector(`[data-path="${path}"]`).closest('.js-tag-list').remove();
+ },
});
diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js
index b3158f7e939..11a19a673b1 100644
--- a/app/assets/javascripts/pages/projects/tags/new/index.js
+++ b/app/assets/javascripts/pages/projects/tags/new/index.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import GLForm from '../../../../gl_form';
import RefSelectDropdown from '../../../../ref_select_dropdown';
import ZenMode from '../../../../zen_mode';
-import GLForm from '../../../../gl_form';
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/tags/releases/index.js b/app/assets/javascripts/pages/projects/tags/releases/index.js
index d6afc71fb03..abdc97f62d0 100644
--- a/app/assets/javascripts/pages/projects/tags/releases/index.js
+++ b/app/assets/javascripts/pages/projects/tags/releases/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import ZenMode from '~/zen_mode';
import GLForm from '~/gl_form';
+import ZenMode from '~/zen_mode';
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/tags/remove_tag.js b/app/assets/javascripts/pages/projects/tags/remove_tag.js
index 7e83dbe0565..7b95560df7b 100644
--- a/app/assets/javascripts/pages/projects/tags/remove_tag.js
+++ b/app/assets/javascripts/pages/projects/tags/remove_tag.js
@@ -1,6 +1,6 @@
+import initConfirmModal from '~/confirm_modal';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import initConfirmModal from '~/confirm_modal';
export const initRemoveTag = ({ onDelete = () => {} }) => {
return initConfirmModal({
diff --git a/app/assets/javascripts/pages/projects/tags/show/index.js b/app/assets/javascripts/pages/projects/tags/show/index.js
index 651cc05ca4f..6f5406f554f 100644
--- a/app/assets/javascripts/pages/projects/tags/show/index.js
+++ b/app/assets/javascripts/pages/projects/tags/show/index.js
@@ -1,10 +1,8 @@
import { redirectTo, getBaseURL, stripFinalUrlSegment } from '~/lib/utils/url_utility';
import { initRemoveTag } from '../remove_tag';
-document.addEventListener('DOMContentLoaded', () => {
- initRemoveTag({
- onDelete: (path = '') => {
- redirectTo(stripFinalUrlSegment([getBaseURL(), path].join('')));
- },
- });
+initRemoveTag({
+ onDelete: (path = '') => {
+ redirectTo(stripFinalUrlSegment([getBaseURL(), path].join('')));
+ },
});
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index 9c75531ca40..dead61cf358 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -1,3 +1,3 @@
import initWikis from '~/pages/shared/wikis';
-document.addEventListener('DOMContentLoaded', initWikis);
+initWikis();
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index a33d11f3613..4104025aa59 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -1,6 +1,6 @@
+import NoEmojiValidator from '~/emoji/no_emoji_validator';
import LengthValidator from '~/pages/sessions/new/length_validator';
import UsernameValidator from '~/pages/sessions/new/username_validator';
-import NoEmojiValidator from '~/emoji/no_emoji_validator';
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js
index b6171e08e01..a8c288c3663 100644
--- a/app/assets/javascripts/pages/search/show/index.js
+++ b/app/assets/javascripts/pages/search/show/index.js
@@ -1,7 +1,5 @@
-import Search from './search';
import { initSearchApp } from '~/search';
document.addEventListener('DOMContentLoaded', () => {
- initSearchApp(); // Vue Bootstrap
- return new Search(); // Legacy Search Methods
+ initSearchApp();
});
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
deleted file mode 100644
index cbef5ab1bbc..00000000000
--- a/app/assets/javascripts/pages/search/show/search.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import $ from 'jquery';
-import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
-import Project from '~/pages/projects/project';
-import { visitUrl } from '~/lib/utils/url_utility';
-import refreshCounts from './refresh_counts';
-
-export default class Search {
- constructor() {
- this.searchInput = '.js-search-input';
- this.searchClear = '.js-search-clear';
-
- setHighlightClass(); // Code Highlighting
- this.eventListeners(); // Search Form Actions
- refreshCounts(); // Other Scope Tab Counts
- Project.initRefSwitcher(); // Code Search Branch Picker
- }
-
- eventListeners() {
- $(document).off('keyup', this.searchInput).on('keyup', this.searchInput, this.searchKeyUp);
- $(document)
- .off('click', this.searchClear)
- .on('click', this.searchClear, this.clearSearchField.bind(this));
-
- $('a.js-search-clear').off('click', this.clearSearchFilter).on('click', this.clearSearchFilter);
- }
-
- static submitSearch() {
- return $('.js-search-form').submit();
- }
-
- searchKeyUp() {
- const $input = $(this);
- if ($input.val() === '') {
- $('.js-search-clear').addClass('hidden');
- } else {
- $('.js-search-clear').removeClass('hidden');
- }
- }
-
- clearSearchField() {
- return $(this.searchInput).val('').trigger('keyup').focus();
- }
-
- // We need to manually follow the link on the anchors
- // that have this event bound, as their `click` default
- // behavior is prevented by the toggle logic.
- /* eslint-disable-next-line class-methods-use-this */
- clearSearchFilter(ev) {
- const $target = $(ev.currentTarget);
-
- visitUrl($target.href);
- ev.stopPropagation();
- }
-}
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 55bc93a2b13..d39f56cfd03 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -1,10 +1,10 @@
import $ from 'jquery';
-import LengthValidator from './length_validator';
-import UsernameValidator from './username_validator';
import NoEmojiValidator from '../../../emoji/no_emoji_validator';
-import SigninTabsMemoizer from './signin_tabs_memoizer';
+import LengthValidator from './length_validator';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
+import SigninTabsMemoizer from './signin_tabs_memoizer';
+import UsernameValidator from './username_validator';
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index f3b0948a40d..338fe1b66f2 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -1,9 +1,9 @@
import { debounce } from 'lodash';
-import InputValidator from '~/validators/input_validator';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
+import InputValidator from '~/validators/input_validator';
const debounceTimeoutDuration = 1000;
const rootUrl = gon.relative_url_root;
diff --git a/app/assets/javascripts/pages/shared/mount_runner_instructions.js b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
new file mode 100644
index 00000000000..51028e585b8
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import InstallRunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
+
+Vue.use(VueApollo);
+
+export function initInstallRunner(componentId = 'js-install-runner') {
+ const installRunnerEl = document.getElementById(componentId);
+
+ if (installRunnerEl) {
+ const defaultClient = createDefaultClient();
+ const { projectPath, groupPath } = installRunnerEl.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient,
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: installRunnerEl,
+ apolloProvider,
+ provide: {
+ projectPath,
+ groupPath,
+ },
+ render(createElement) {
+ return createElement(InstallRunnerInstructions);
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/pages/shared/wikis/index.js b/app/assets/javascripts/pages/shared/wikis/index.js
index 5e23b9bab4e..c8dc75828e4 100644
--- a/app/assets/javascripts/pages/shared/wikis/index.js
+++ b/app/assets/javascripts/pages/shared/wikis/index.js
@@ -1,12 +1,12 @@
import $ from 'jquery';
import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
-import csrf from '~/lib/utils/csrf';
import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki';
-import Wikis from './wikis';
-import ZenMode from '../../../zen_mode';
+import csrf from '~/lib/utils/csrf';
+import Translate from '~/vue_shared/translate';
import GLForm from '../../../gl_form';
+import ZenMode from '../../../zen_mode';
import deleteWikiModal from './components/delete_wiki_modal.vue';
+import Wikis from './wikis';
export default () => {
new Wikis(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 149e666256b..3ff455fad32 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -1,11 +1,11 @@
-import $ from 'jquery';
-import { last } from 'lodash';
import { scaleLinear, scaleThreshold } from 'd3-scale';
import { select } from 'd3-selection';
import dateFormat from 'dateformat';
-import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
-import axios from '~/lib/utils/axios_utils';
+import $ from 'jquery';
+import { last } from 'lodash';
import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import { n__, s__, __ } from '~/locale';
const d3 = { select, scaleLinear, scaleThreshold };
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 7c88aa53e4b..80e14842f51 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -1,9 +1,9 @@
-import $ from 'jquery';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import axios from '~/lib/utils/axios_utils';
+import $ from 'jquery';
import Activities from '~/activities';
-import { localTimeAgo } from '~/lib/utils/datetime_utility';
import AjaxCache from '~/lib/utils/ajax_cache';
+import axios from '~/lib/utils/axios_utils';
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import ActivityCalendar from './activity_calendar';
import UserOverviewBlock from './user_overview_block';
@@ -141,7 +141,15 @@ export default class UserTabs {
this.loadOverviewTab();
}
- const loadableActions = ['groups', 'contributed', 'projects', 'starred', 'snippets'];
+ const loadableActions = [
+ 'groups',
+ 'contributed',
+ 'projects',
+ 'starred',
+ 'snippets',
+ 'followers',
+ 'following',
+ ];
if (loadableActions.indexOf(action) > -1) {
this.loadTab(action, endpoint);
}
diff --git a/app/assets/javascripts/performance_bar/components/add_request.vue b/app/assets/javascripts/performance_bar/components/add_request.vue
index 54bca8a1b67..d48a5acb85c 100644
--- a/app/assets/javascripts/performance_bar/components/add_request.vue
+++ b/app/assets/javascripts/performance_bar/components/add_request.vue
@@ -27,7 +27,7 @@ export default {
<div id="peek-view-add-request" class="view">
<form class="form-inline" @submit.prevent>
<button
- class="btn-blank btn-link bold"
+ class="btn-blank btn-link bold gl-text-blue-300"
type="button"
:title="__(`Add request manually`)"
@click="toggleInput"
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 930c5e50511..de4bbb36141 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlIcon, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import RequestWarning from './request_warning.vue';
export default {
@@ -7,7 +7,6 @@ export default {
RequestWarning,
GlButton,
GlModal,
- GlIcon,
},
directives: {
'gl-modal': GlModalDirective,
@@ -94,10 +93,10 @@ export default {
data-qa-selector="detailed_metric_content"
>
<gl-button v-gl-modal="modalId" class="gl-mr-2" type="button" variant="link">
- {{ metricDetailsLabel }}
+ <span class="gl-text-blue-300 gl-font-weight-bold">{{ metricDetailsLabel }}</span>
</gl-button>
- <gl-modal :modal-id="modalId" :title="header" size="lg" modal-class="gl-mt-7" scrollable>
- <table class="table">
+ <gl-modal :modal-id="modalId" :title="header" size="lg" footer-class="d-none" scrollable>
+ <table class="table gl-table">
<template v-if="detailsList.length">
<tr v-for="(item, index) in detailsList" :key="index">
<td>
@@ -116,14 +115,16 @@ export default {
{{ item[key] }}
<gl-button
v-if="keyIndex == 0 && item.backtrace"
- class="gl-ml-3"
+ class="gl-ml-3 button-ellipsis-horizontal"
data-testid="backtrace-expand-btn"
- type="button"
+ category="primary"
+ variant="default"
+ icon="ellipsis_h"
+ size="small"
+ :selected="itemHasOpenedBacktrace(index)"
:aria-label="__('Toggle backtrace')"
@click="toggleBacktrace(index)"
- >
- <gl-icon :size="12" name="ellipsis_h" />
- </gl-button>
+ />
</div>
<pre v-if="itemHasOpenedBacktrace(index)" class="backtrace-row mt-2">{{
item.backtrace
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index e38771785b7..85789cd1fdf 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -2,10 +2,10 @@
/* eslint-disable vue/no-v-html */
import { glEmojiTag } from '~/emoji';
+import { s__ } from '~/locale';
import AddRequest from './add_request.vue';
import DetailedMetric from './detailed_metric.vue';
import RequestSelector from './request_selector.vue';
-import { s__ } from '~/locale';
export default {
components: {
@@ -64,6 +64,11 @@ export default {
keys: ['request', 'body'],
},
{
+ metric: 'external-http',
+ header: s__('PerformanceBar|External Http calls'),
+ keys: ['label', 'code', 'proxy', 'error'],
+ },
+ {
metric: 'total',
header: s__('PerformanceBar|Frontend resources'),
keys: ['name', 'size'],
@@ -120,7 +125,7 @@ export default {
<div id="js-peek" :class="env">
<div
v-if="currentRequest"
- class="d-flex container-fluid container-limited"
+ class="d-flex container-fluid container-limited justify-content-center"
data-qa-selector="performance_bar"
>
<div id="peek-view-host" class="view">
@@ -147,11 +152,15 @@ export default {
id="peek-view-trace"
class="view"
>
- <a :href="currentRequest.details.tracing.tracing_url">{{ s__('PerformanceBar|trace') }}</a>
+ <a class="gl-text-blue-300" :href="currentRequest.details.tracing.tracing_url">{{
+ s__('PerformanceBar|trace')
+ }}</a>
</div>
<add-request v-on="$listeners" />
<div v-if="currentRequest.details" id="peek-download" class="view">
- <a :download="downloadName" :href="downloadPath">{{ s__('PerformanceBar|Download') }}</a>
+ <a class="gl-text-blue-300" :download="downloadName" :href="downloadPath">{{
+ s__('PerformanceBar|Download')
+ }}</a>
</div>
<request-selector
v-if="currentRequest"
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index 0d5c294ea56..522e34753e9 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -1,13 +1,12 @@
/* eslint-disable @gitlab/require-i18n-strings */
import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
import axios from '~/lib/utils/axios_utils';
+import Translate from '~/vue_shared/translate';
+import initPerformanceBarLog from './performance_bar_log';
import PerformanceBarService from './services/performance_bar_service';
import PerformanceBarStore from './stores/performance_bar_store';
-import initPerformanceBarLog from './performance_bar_log';
-
Vue.use(Translate);
const initPerformanceBar = (el) => {
diff --git a/app/assets/javascripts/performance_bar/performance_bar_log.js b/app/assets/javascripts/performance_bar/performance_bar_log.js
index c61b0cb32e8..aad99e2604e 100644
--- a/app/assets/javascripts/performance_bar/performance_bar_log.js
+++ b/app/assets/javascripts/performance_bar/performance_bar_log.js
@@ -43,7 +43,7 @@ const logUserTimingMetrics = () => {
const initPerformanceBarLog = () => {
console.log(
`%c ${String.fromCodePoint(0x1f98a)} GitLab performance bar`,
- 'width:100%;background-color: #292961; color: #FFFFFF; font-size:24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; padding: 10px;display:block;padding-right: 100px;',
+ 'width:100%; background-color: #292961; color: #FFFFFF; padding: 10px; display:block;',
);
initVitalsLog();
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
index 38255b3a37d..a614342c858 100644
--- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -1,5 +1,5 @@
-import axios from '../../lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
+import axios from '../../lib/utils/axios_utils';
export default class PerformanceBarService {
static interceptor = null;
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index db42966d159..e845c8b9df4 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -1,7 +1,7 @@
-import { parseBoolean } from './lib/utils/common_utils';
+import { deprecatedCreateFlash as Flash } from './flash';
import axios from './lib/utils/axios_utils';
+import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
-import { deprecatedCreateFlash as Flash } from './flash';
const DEFERRED_LINK_CLASS = 'deferred-link';
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
new file mode 100644
index 00000000000..b40c9a48903
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
@@ -0,0 +1,114 @@
+<script>
+import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
+import { __, s__, sprintf } from '~/locale';
+import { COMMIT_FAILURE, COMMIT_SUCCESS } from '../../constants';
+import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
+import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
+
+import CommitForm from './commit_form.vue';
+
+const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
+const MR_TARGET_BRANCH = 'merge_request[target_branch]';
+
+export default {
+ alertTexts: {
+ [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
+ [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
+ },
+ i18n: {
+ defaultCommitMessage: __('Update %{sourcePath} file'),
+ },
+ components: {
+ CommitForm,
+ },
+ inject: ['projectFullPath', 'ciConfigPath', 'defaultBranch', 'newMergeRequestPath'],
+ props: {
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ commit: {},
+ isSaving: false,
+ };
+ },
+ apollo: {
+ commitSha: {
+ query: getCommitSha,
+ },
+ },
+ computed: {
+ defaultCommitMessage() {
+ return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
+ },
+ },
+ methods: {
+ redirectToNewMergeRequest(sourceBranch) {
+ const url = mergeUrlParams(
+ {
+ [MR_SOURCE_BRANCH]: sourceBranch,
+ [MR_TARGET_BRANCH]: this.defaultBranch,
+ },
+ this.newMergeRequestPath,
+ );
+ redirectTo(url);
+ },
+ async onCommitSubmit({ message, branch, openMergeRequest }) {
+ this.isSaving = true;
+
+ try {
+ const {
+ data: {
+ commitCreate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: commitCIFile,
+ variables: {
+ projectPath: this.projectFullPath,
+ branch,
+ startBranch: this.defaultBranch,
+ message,
+ filePath: this.ciConfigPath,
+ content: this.ciFileContent,
+ lastCommitId: this.commitSha,
+ },
+ update(store, { data }) {
+ const commitSha = data?.commitCreate?.commit?.sha;
+
+ if (commitSha) {
+ store.writeQuery({ query: getCommitSha, data: { commitSha } });
+ }
+ },
+ });
+
+ if (errors?.length) {
+ this.$emit('showError', { type: COMMIT_FAILURE, reasons: errors });
+ } else if (openMergeRequest) {
+ this.redirectToNewMergeRequest(branch);
+ } else {
+ this.$emit('commit', { type: COMMIT_SUCCESS });
+ }
+ } catch (error) {
+ this.$emit('showError', { type: COMMIT_FAILURE, reasons: [error?.message] });
+ } finally {
+ this.isSaving = false;
+ }
+ },
+ onCommitCancel() {
+ this.$emit('resetContent');
+ },
+ },
+};
+</script>
+
+<template>
+ <commit-form
+ :default-branch="defaultBranch"
+ :default-message="defaultCommitMessage"
+ :is-saving="isSaving"
+ @cancel="onCommitCancel"
+ @submit="onCommitSubmit"
+ />
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
new file mode 100644
index 00000000000..007faa4ed0d
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
@@ -0,0 +1,100 @@
+<script>
+import { GlAlert, GlIcon } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { __, s__ } from '~/locale';
+import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
+import { DEFAULT, INVALID_CI_CONFIG } from '~/pipelines/constants';
+import EditorLite from '~/vue_shared/components/editor_lite.vue';
+
+export default {
+ i18n: {
+ viewOnlyMessage: s__('Pipelines|Merged YAML is view only'),
+ },
+ errorTexts: {
+ [INVALID_CI_CONFIG]: __('Your CI configuration file is invalid.'),
+ [DEFAULT]: __('An unknown error occurred.'),
+ },
+ components: {
+ EditorLite,
+ GlAlert,
+ GlIcon,
+ },
+ inject: ['ciConfigPath'],
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ failureType: null,
+ };
+ },
+ computed: {
+ failure() {
+ switch (this.failureType) {
+ case INVALID_CI_CONFIG:
+ return this.$options.errorTexts[INVALID_CI_CONFIG];
+ default:
+ return this.$options.errorTexts[DEFAULT];
+ }
+ },
+ fileGlobalId() {
+ return `${this.ciConfigPath}-${uniqueId()}`;
+ },
+ hasError() {
+ return this.failureType;
+ },
+ isInvalidConfiguration() {
+ return this.ciConfigData.status === CI_CONFIG_STATUS_INVALID;
+ },
+ mergedYaml() {
+ return this.ciConfigData.mergedYaml;
+ },
+ },
+ watch: {
+ ciConfigData: {
+ immediate: true,
+ handler() {
+ if (this.isInvalidConfiguration) {
+ this.reportFailure(INVALID_CI_CONFIG);
+ } else if (this.hasError) {
+ this.resetFailure();
+ }
+ },
+ },
+ },
+ methods: {
+ reportFailure(errorType) {
+ this.failureType = errorType;
+ },
+ resetFailure() {
+ this.failureType = null;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-alert v-if="hasError" variant="danger" :dismissible="false">
+ {{ failure }}
+ </gl-alert>
+ <div v-else>
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-icon :size="18" name="lock" class="gl-text-gray-500 gl-mr-3" />
+ {{ $options.i18n.viewOnlyMessage }}
+ </div>
+ <div class="gl-mt-3 gl-border-solid gl-border-gray-100 gl-border-1">
+ <editor-lite
+ ref="editor"
+ :value="mergedYaml"
+ :file-name="ciConfigPath"
+ :file-global-id="fileGlobalId"
+ :editor-options="{ readOnly: true }"
+ v-on="$listeners"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
index b8d49d77ea9..872da88d3e6 100644
--- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue
+++ b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
@@ -1,26 +1,30 @@
<script>
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import { EDITOR_READY_EVENT } from '~/editor/constants';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
+import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
export default {
components: {
EditorLite,
},
- inject: ['projectPath', 'projectNamespace'],
+ inject: ['ciConfigPath', 'projectPath', 'projectNamespace'],
inheritAttrs: false,
- props: {
- ciConfigPath: {
- type: String,
- required: true,
- },
+ data() {
+ return {
+ commitSha: '',
+ };
+ },
+ apollo: {
commitSha: {
- type: String,
- required: false,
- default: null,
+ query: getCommitSha,
},
},
methods: {
- onEditorReady() {
+ onCiConfigUpdate(content) {
+ this.$emit('updateCiConfig', content);
+ },
+ registerCiSchema() {
const editorInstance = this.$refs.editor.getEditor();
editorInstance.use(new CiSchemaExtension());
@@ -31,6 +35,7 @@ export default {
});
},
},
+ readyEvent: EDITOR_READY_EVENT,
};
</script>
<template>
@@ -39,7 +44,8 @@ export default {
ref="editor"
:file-name="ciConfigPath"
v-bind="$attrs"
- @editor-ready="onEditorReady"
+ @[$options.readyEvent]="registerCiSchema"
+ @input="onCiConfigUpdate"
v-on="$listeners"
/>
</div>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
new file mode 100644
index 00000000000..ab41c0170e9
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
@@ -0,0 +1,30 @@
+<script>
+import ValidationSegment from './validation_segment.vue';
+
+export default {
+ validationSegmentClasses:
+ 'gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base',
+ components: {
+ ValidationSegment,
+ },
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ isCiConfigDataLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-mb-5">
+ <validation-segment
+ :class="$options.validationSegmentClasses"
+ :loading="isCiConfigDataLoading"
+ :ci-config="ciConfigData"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
index 22f378c571a..94fb3a66fdd 100644
--- a/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
@@ -1,8 +1,8 @@
<script>
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
-import { CI_CONFIG_STATUS_VALID } from '../../constants';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import { CI_CONFIG_STATUS_VALID } from '../../constants';
export const i18n = {
learnMore: __('Learn more'),
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
index 58a96c3f725..5d9697c9427 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
+++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert, GlLink, GlSprintf, GlTable } from '@gitlab/ui';
-import CiLintWarnings from './ci_lint_warnings.vue';
-import CiLintResultsValue from './ci_lint_results_value.vue';
-import CiLintResultsParam from './ci_lint_results_param.vue';
import { __ } from '~/locale';
+import CiLintResultsParam from './ci_lint_results_param.vue';
+import CiLintResultsValue from './ci_lint_results_value.vue';
+import CiLintWarnings from './ci_lint_warnings.vue';
const thBorderColor = 'gl-border-gray-100!';
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue
index 23808bcb292..49225a7cac7 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue
+++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
export default {
props: {
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
new file mode 100644
index 00000000000..3bdcf383bee
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -0,0 +1,121 @@
+<script>
+import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import {
+ CI_CONFIG_STATUS_INVALID,
+ CREATE_TAB,
+ LINT_TAB,
+ MERGED_TAB,
+ VISUALIZE_TAB,
+} from '../constants';
+import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
+import TextEditor from './editor/text_editor.vue';
+import CiLint from './lint/ci_lint.vue';
+import EditorTab from './ui/editor_tab.vue';
+
+export default {
+ i18n: {
+ tabEdit: s__('Pipelines|Write pipeline configuration'),
+ tabGraph: s__('Pipelines|Visualize'),
+ tabLint: s__('Pipelines|Lint'),
+ tabMergedYaml: s__('Pipelines|View merged YAML'),
+ },
+ errorTexts: {
+ loadMergedYaml: s__('Pipelines|Could not load merged YAML content'),
+ },
+ tabConstants: {
+ CREATE_TAB,
+ LINT_TAB,
+ MERGED_TAB,
+ VISUALIZE_TAB,
+ },
+ components: {
+ CiConfigMergedPreview,
+ CiLint,
+ EditorTab,
+ GlAlert,
+ GlLoadingIcon,
+ GlTab,
+ GlTabs,
+ PipelineGraph,
+ TextEditor,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ isCiConfigDataLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ hasMergedYamlLoadError() {
+ return (
+ !this.ciConfigData?.mergedYaml && this.ciConfigData.status !== CI_CONFIG_STATUS_INVALID
+ );
+ },
+ },
+ methods: {
+ setCurrentTab(tabName) {
+ this.$emit('set-current-tab', tabName);
+ },
+ },
+};
+</script>
+<template>
+ <gl-tabs class="file-editor gl-mb-3">
+ <editor-tab
+ class="gl-mb-3"
+ :title="$options.i18n.tabEdit"
+ lazy
+ data-testid="editor-tab"
+ @click="setCurrentTab($options.tabConstants.CREATE_TAB)"
+ >
+ <text-editor :value="ciFileContent" v-on="$listeners" />
+ </editor-tab>
+ <gl-tab
+ v-if="glFeatures.ciConfigVisualizationTab"
+ class="gl-mb-3"
+ :title="$options.i18n.tabGraph"
+ lazy
+ data-testid="visualization-tab"
+ @click="setCurrentTab($options.tabConstants.VISUALIZE_TAB)"
+ >
+ <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
+ <pipeline-graph v-else :pipeline-data="ciConfigData" />
+ </gl-tab>
+ <editor-tab
+ class="gl-mb-3"
+ :title="$options.i18n.tabLint"
+ data-testid="lint-tab"
+ @click="setCurrentTab($options.tabConstants.LINT_TAB)"
+ >
+ <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
+ <ci-lint v-else :ci-config="ciConfigData" />
+ </editor-tab>
+ <gl-tab
+ v-if="glFeatures.ciConfigMergedTab"
+ class="gl-mb-3"
+ :title="$options.i18n.tabMergedYaml"
+ lazy
+ data-testid="merged-tab"
+ @click="setCurrentTab($options.tabConstants.MERGED_TAB)"
+ >
+ <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
+ <gl-alert v-else-if="hasMergedYamlLoadError" variant="danger" :dismissible="false">
+ {{ $options.errorTexts.loadMergedYaml }}
+ </gl-alert>
+ <ci-config-merged-preview v-else :ci-config-data="ciConfigData" v-on="$listeners" />
+ </gl-tab>
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue b/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
new file mode 100644
index 00000000000..bc076fbe349
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
@@ -0,0 +1,26 @@
+<script>
+export default {
+ props: {
+ hasUnsavedChanges: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ created() {
+ window.addEventListener('beforeunload', this.confirmChanges);
+ },
+ destroyed() {
+ window.removeEventListener('beforeunload', this.confirmChanges);
+ },
+ methods: {
+ confirmChanges(e = {}) {
+ if (this.hasUnsavedChanges) {
+ e.preventDefault();
+ // eslint-disable-next-line no-param-reassign
+ e.returnValue = ''; // Chrome requires returnValue to be set
+ }
+ },
+ },
+ render: () => null,
+};
+</script>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index 70bab8092c0..e676fdeae02 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -1,2 +1,16 @@
export const CI_CONFIG_STATUS_VALID = 'VALID';
export const CI_CONFIG_STATUS_INVALID = 'INVALID';
+
+export const COMMIT_FAILURE = 'COMMIT_FAILURE';
+export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
+
+export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
+export const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
+export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
+
+export const CREATE_TAB = 'CREATE_TAB';
+export const LINT_TAB = 'LINT_TAB';
+export const MERGED_TAB = 'MERGED_TAB';
+export const VISUALIZE_TAB = 'VISUALIZE_TAB';
+
+export const TABS_WITH_COMMIT_FORM = [CREATE_TAB, LINT_TAB, VISUALIZE_TAB];
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
index 0c58749a8b2..2af0cd5f6d4 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
@@ -1,4 +1,4 @@
-mutation commitCIFileMutation(
+mutation commitCIFile(
$projectPath: ID!
$branch: String!
$startBranch: String
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
index dfddb29701d..30c18a96536 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
@@ -3,6 +3,7 @@
query getCiConfigData($projectPath: ID!, $content: String!) {
ciConfig(projectPath: $projectPath, content: $content) {
errors
+ mergedYaml
status
stages {
...PipelineStagesConnection
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql
new file mode 100644
index 00000000000..6c7635887ec
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql
@@ -0,0 +1,3 @@
+query getCommitSha {
+ commitSha @client
+}
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 583ba555080..dc427f55b5f 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -2,12 +2,17 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import typeDefs from './graphql/typedefs.graphql';
+import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import { resolvers } from './graphql/resolvers';
-
+import typeDefs from './graphql/typedefs.graphql';
import PipelineEditorApp from './pipeline_editor_app.vue';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
+ // Prevent issues loading syntax validation workers
+ // Fixes https://gitlab.com/gitlab-org/gitlab/-/issues/297252
+ // TODO Remove when https://gitlab.com/gitlab-org/gitlab/-/issues/321656 is resolved
+ resetServiceWorkersPublicPath();
+
const el = document.querySelector(selector);
if (!el) {
@@ -15,14 +20,13 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
}
const {
- // props
- ciConfigPath,
+ // Add to apollo cache as it can be updated by future queries
commitSha,
+ // Add to provide/inject API for static values
+ ciConfigPath,
defaultBranch,
- newMergeRequestPath,
-
- // `provide/inject` data
lintHelpPagePath,
+ newMergeRequestPath,
projectFullPath,
projectPath,
projectNamespace,
@@ -35,25 +39,27 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
defaultClient: createDefaultClient(resolvers, { typeDefs }),
});
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ commitSha,
+ },
+ });
+
return new Vue({
el,
apolloProvider,
provide: {
+ ciConfigPath,
+ defaultBranch,
lintHelpPagePath,
+ newMergeRequestPath,
projectFullPath,
projectPath,
projectNamespace,
ymlHelpPagePath,
},
render(h) {
- return h(PipelineEditorApp, {
- props: {
- ciConfigPath,
- commitSha,
- defaultBranch,
- newMergeRequestPath,
- },
- });
+ return h(PipelineEditorApp);
},
});
};
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 21993e2120a..b4a818e2472 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -1,84 +1,55 @@
<script>
-import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
-import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status';
+import { __, s__, sprintf } from '~/locale';
-import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import CiLint from './components/lint/ci_lint.vue';
-import CommitForm from './components/commit/commit_form.vue';
-import EditorTab from './components/ui/editor_tab.vue';
-import TextEditor from './components/text_editor.vue';
-import ValidationSegment from './components/info/validation_segment.vue';
-
-import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql';
+import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
+import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
+import {
+ COMMIT_FAILURE,
+ COMMIT_SUCCESS,
+ DEFAULT_FAILURE,
+ LOAD_FAILURE_NO_FILE,
+ LOAD_FAILURE_UNKNOWN,
+} from './constants';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
-import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
-
-const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
-const MR_TARGET_BRANCH = 'merge_request[target_branch]';
-
-const COMMIT_FAILURE = 'COMMIT_FAILURE';
-const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
-const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
-const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
-const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
+import PipelineEditorHome from './pipeline_editor_home.vue';
export default {
components: {
- CiLint,
- CommitForm,
- EditorTab,
+ ConfirmUnsavedChangesDialog,
GlAlert,
GlLoadingIcon,
- GlTabs,
- GlTab,
- PipelineGraph,
- TextEditor,
- ValidationSegment,
+ PipelineEditorHome,
},
- mixins: [glFeatureFlagsMixin()],
- inject: ['projectFullPath'],
- props: {
- defaultBranch: {
- type: String,
- required: false,
- default: null,
+ inject: {
+ ciConfigPath: {
+ default: '',
},
- commitSha: {
- type: String,
- required: false,
+ defaultBranch: {
default: null,
},
- ciConfigPath: {
- type: String,
- required: true,
- },
- newMergeRequestPath: {
- type: String,
- required: true,
+ projectFullPath: {
+ default: '',
},
},
data() {
return {
ciConfigData: {},
- content: '',
- contentModel: '',
- lastCommitSha: this.commitSha,
- isSaving: false,
-
// Success and failure state
failureType: null,
- showFailureAlert: false,
failureReasons: [],
- successType: null,
+ initialCiFileContent: '',
+ lastCommittedContent: '',
+ currentCiFileContent: '',
+ showFailureAlert: false,
showSuccessAlert: false,
+ successType: null,
};
},
apollo: {
- content: {
+ initialCiFileContent: {
query: getBlobContent,
variables() {
return {
@@ -91,7 +62,10 @@ export default {
return data?.blobContent?.rawData;
},
result({ data }) {
- this.contentModel = data?.blobContent?.rawData ?? '';
+ const fileContent = data?.blobContent?.rawData ?? '';
+
+ this.lastCommittedContent = fileContent;
+ this.currentCiFileContent = fileContent;
},
error(error) {
this.handleBlobContentError(error);
@@ -100,13 +74,13 @@ export default {
ciConfigData: {
query: getCiConfigData,
// If content is not loaded, we can't lint the data
- skip: ({ contentModel }) => {
- return !contentModel;
+ skip: ({ currentCiFileContent }) => {
+ return !currentCiFileContent;
},
variables() {
return {
projectPath: this.projectFullPath,
- content: this.contentModel,
+ content: this.currentCiFileContent,
};
},
update(data) {
@@ -122,8 +96,11 @@ export default {
},
},
computed: {
+ hasUnsavedChanges() {
+ return this.lastCommittedContent !== this.currentCiFileContent;
+ },
isBlobContentLoading() {
- return this.$apollo.queries.content.loading;
+ return this.$apollo.queries.initialCiFileContent.loading;
},
isBlobContentError() {
return this.failureType === LOAD_FAILURE_NO_FILE;
@@ -131,62 +108,60 @@ export default {
isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading;
},
- defaultCommitMessage() {
- return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
- },
- success() {
- switch (this.successType) {
- case COMMIT_SUCCESS:
- return {
- text: this.$options.alertTexts[COMMIT_SUCCESS],
- variant: 'info',
- };
- default:
- return null;
- }
- },
failure() {
switch (this.failureType) {
case LOAD_FAILURE_NO_FILE:
return {
- text: sprintf(this.$options.alertTexts[LOAD_FAILURE_NO_FILE], {
+ text: sprintf(this.$options.errorTexts[LOAD_FAILURE_NO_FILE], {
filePath: this.ciConfigPath,
}),
variant: 'danger',
};
case LOAD_FAILURE_UNKNOWN:
return {
- text: this.$options.alertTexts[LOAD_FAILURE_UNKNOWN],
+ text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
variant: 'danger',
};
case COMMIT_FAILURE:
return {
- text: this.$options.alertTexts[COMMIT_FAILURE],
+ text: this.$options.errorTexts[COMMIT_FAILURE],
variant: 'danger',
};
default:
return {
- text: this.$options.alertTexts[DEFAULT_FAILURE],
+ text: this.$options.errorTexts[DEFAULT_FAILURE],
variant: 'danger',
};
}
},
+ success() {
+ switch (this.successType) {
+ case COMMIT_SUCCESS:
+ return {
+ text: this.$options.successTexts[COMMIT_SUCCESS],
+ variant: 'info',
+ };
+ default:
+ return null;
+ }
+ },
},
i18n: {
- defaultCommitMessage: __('Update %{sourcePath} file'),
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
- alertTexts: {
+ errorTexts: {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
- [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_NO_FILE]: s__(
'Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again.',
),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
},
+ successTexts: {
+ [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
+ },
methods: {
handleBlobContentError(error = {}) {
const { networkError } = error;
@@ -207,72 +182,32 @@ export default {
dismissFailure() {
this.showFailureAlert = false;
},
+ dismissSuccess() {
+ this.showSuccessAlert = false;
+ },
reportFailure(type, reasons = []) {
this.showFailureAlert = true;
this.failureType = type;
this.failureReasons = reasons;
},
- dismissSuccess() {
- this.showSuccessAlert = false;
- },
reportSuccess(type) {
this.showSuccessAlert = true;
this.successType = type;
},
-
- redirectToNewMergeRequest(sourceBranch) {
- const url = mergeUrlParams(
- {
- [MR_SOURCE_BRANCH]: sourceBranch,
- [MR_TARGET_BRANCH]: this.defaultBranch,
- },
- this.newMergeRequestPath,
- );
- redirectTo(url);
+ resetContent() {
+ this.currentCiFileContent = this.lastCommittedContent;
},
- async onCommitSubmit(event) {
- this.isSaving = true;
- const { message, branch, openMergeRequest } = event;
-
- try {
- const {
- data: {
- commitCreate: { errors, commit },
- },
- } = await this.$apollo.mutate({
- mutation: commitCiFileMutation,
- variables: {
- projectPath: this.projectFullPath,
- branch,
- startBranch: this.defaultBranch,
- message,
- filePath: this.ciConfigPath,
- content: this.contentModel,
- lastCommitId: this.lastCommitSha,
- },
- });
-
- if (errors?.length) {
- this.reportFailure(COMMIT_FAILURE, errors);
- return;
- }
-
- if (openMergeRequest) {
- this.redirectToNewMergeRequest(branch);
- } else {
- this.reportSuccess(COMMIT_SUCCESS);
-
- // Update latest commit
- this.lastCommitSha = commit.sha;
- }
- } catch (error) {
- this.reportFailure(COMMIT_FAILURE, [error?.message]);
- } finally {
- this.isSaving = false;
- }
+ showErrorAlert({ type, reasons = [] }) {
+ this.reportFailure(type, reasons);
},
- onCommitCancel() {
- this.contentModel = this.content;
+ updateCiConfig(ciFileContent) {
+ this.currentCiFileContent = ciFileContent;
+ },
+ updateOnCommit({ type }) {
+ this.reportSuccess(type);
+ // Keep track of the latest commited content to know
+ // if the user has made changes to the file that are unsaved.
+ this.lastCommittedContent = this.currentCiFileContent;
},
},
};
@@ -280,20 +215,10 @@ export default {
<template>
<div class="gl-mt-4">
- <gl-alert
- v-if="showSuccessAlert"
- :variant="success.variant"
- :dismissible="true"
- @dismiss="dismissSuccess"
- >
+ <gl-alert v-if="showSuccessAlert" :variant="success.variant" @dismiss="dismissSuccess">
{{ success.text }}
</gl-alert>
- <gl-alert
- v-if="showFailureAlert"
- :variant="failure.variant"
- :dismissible="true"
- @dismiss="dismissFailure"
- >
+ <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="dismissFailure">
{{ failure.text }}
<ul v-if="failureReasons.length" class="gl-mb-0">
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
@@ -301,46 +226,16 @@ export default {
</gl-alert>
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<div v-else-if="!isBlobContentError" class="gl-mt-4">
- <div class="file-editor gl-mb-3">
- <div class="info-well gl-display-none gl-display-sm-block">
- <validation-segment
- class="well-segment"
- :loading="isCiConfigDataLoading"
- :ci-config="ciConfigData"
- />
- </div>
-
- <gl-tabs>
- <editor-tab :lazy="true" :title="$options.i18n.tabEdit">
- <text-editor
- v-model="contentModel"
- :ci-config-path="ciConfigPath"
- :commit-sha="lastCommitSha"
- />
- </editor-tab>
- <gl-tab
- v-if="glFeatures.ciConfigVisualizationTab"
- :lazy="true"
- :title="$options.i18n.tabGraph"
- data-testid="visualization-tab"
- >
- <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
- <pipeline-graph v-else :pipeline-data="ciConfigData" />
- </gl-tab>
-
- <editor-tab :title="$options.i18n.tabLint">
- <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
- <ci-lint v-else :ci-config="ciConfigData" />
- </editor-tab>
- </gl-tabs>
- </div>
- <commit-form
- :default-branch="defaultBranch"
- :default-message="defaultCommitMessage"
- :is-saving="isSaving"
- @cancel="onCommitCancel"
- @submit="onCommitSubmit"
+ <pipeline-editor-home
+ :is-ci-config-data-loading="isCiConfigDataLoading"
+ :ci-config-data="ciConfigData"
+ :ci-file-content="currentCiFileContent"
+ @commit="updateOnCommit"
+ @resetContent="resetContent"
+ @showError="showErrorAlert"
+ @updateCiConfig="updateCiConfig"
/>
</div>
+ <confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
new file mode 100644
index 00000000000..8c9aad6ed87
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -0,0 +1,60 @@
+<script>
+import CommitSection from './components/commit/commit_section.vue';
+import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
+import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
+import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants';
+
+export default {
+ components: {
+ CommitSection,
+ PipelineEditorHeader,
+ PipelineEditorTabs,
+ },
+ props: {
+ ciConfigData: {
+ type: Object,
+ required: true,
+ },
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ isCiConfigDataLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ currentTab: CREATE_TAB,
+ };
+ },
+ computed: {
+ showCommitForm() {
+ return TABS_WITH_COMMIT_FORM.includes(this.currentTab);
+ },
+ },
+ methods: {
+ setCurrentTab(tabName) {
+ this.currentTab = tabName;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <pipeline-editor-header
+ :ci-config-data="ciConfigData"
+ :is-ci-config-data-loading="isCiConfigDataLoading"
+ />
+ <pipeline-editor-tabs
+ :ci-config-data="ciConfigData"
+ :ci-file-content="ciFileContent"
+ :is-ci-config-data-loading="isCiConfigDataLoading"
+ v-on="$listeners"
+ @set-current-tab="setCurrentTab"
+ />
+ <commit-section v-if="showCommitForm" :ci-file-content="ciFileContent" v-on="$listeners" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index 70c5713b216..5070971c563 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -1,6 +1,4 @@
<script>
-import Vue from 'vue';
-import { uniqueId } from 'lodash';
import {
GlAlert,
GlIcon,
@@ -9,6 +7,7 @@ import {
GlFormGroup,
GlFormInput,
GlFormSelect,
+ GlFormTextarea,
GlLink,
GlDropdown,
GlDropdownItem,
@@ -18,13 +17,15 @@ import {
GlLoadingIcon,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
-import * as Sentry from '~/sentry/wrapper';
-import { s__, __, n__ } from '~/locale';
+import { uniqueId } from 'lodash';
+import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
-import { redirectTo } from '~/lib/utils/url_utility';
-import { VARIABLE_TYPE, FILE_TYPE, CONFIG_VARIABLES_TIMEOUT } from '../constants';
import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { s__, __, n__ } from '~/locale';
+import * as Sentry from '~/sentry/wrapper';
+import { VARIABLE_TYPE, FILE_TYPE, CONFIG_VARIABLES_TIMEOUT } from '../constants';
export default {
typeOptions: [
@@ -38,6 +39,9 @@ export default {
errorTitle: __('Pipeline cannot be run.'),
warningTitle: __('The form contains the following warning:'),
maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
+ // this height value is used inline on the textarea to match the input field height
+ // it's used to prevent the overwrite if 'gl-h-7' or 'gl-h-7!' were used
+ textAreaStyle: { height: '32px' },
components: {
GlAlert,
GlIcon,
@@ -46,6 +50,7 @@ export default {
GlFormGroup,
GlFormInput,
GlFormSelect,
+ GlFormTextarea,
GlLink,
GlDropdown,
GlDropdownItem,
@@ -116,6 +121,7 @@ export default {
totalWarnings: 0,
isWarningDismissed: false,
isLoading: false,
+ submitted: false,
};
},
computed: {
@@ -251,10 +257,6 @@ export default {
return index < this.variables.length - 1;
},
fetchConfigVariables(refValue) {
- if (!gon?.features?.newPipelineFormPrefilledVars) {
- return Promise.resolve({ params: {}, descriptions: {} });
- }
-
this.isLoading = true;
return backOff((next, stop) => {
@@ -298,6 +300,7 @@ export default {
});
},
createPipeline() {
+ this.submitted = true;
const filteredVariables = this.variables
.filter(({ key, value }) => key !== '' && value !== '')
.map(({ variable_type, key, value }) => ({
@@ -317,8 +320,16 @@ export default {
redirectTo(`${this.pipelinesPath}/${data.id}`);
})
.catch((err) => {
- const { errors, warnings, total_warnings: totalWarnings } = err.response.data;
+ // always re-enable submit button
+ this.submitted = false;
+
+ const {
+ errors = [],
+ warnings = [],
+ total_warnings: totalWarnings = 0,
+ } = err?.response?.data;
const [error] = errors;
+
this.error = error;
this.warnings = warnings;
this.totalWarnings = totalWarnings;
@@ -420,10 +431,12 @@ export default {
data-testid="pipeline-form-ci-variable-key"
@change="addEmptyVariable(refFullName)"
/>
- <gl-form-input
+ <gl-form-textarea
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mb-3"
+ :style="$options.textAreaStyle"
+ :no-resize="false"
data-testid="pipeline-form-ci-variable-value"
/>
@@ -436,12 +449,12 @@ export default {
category="secondary"
@click="removeVariable(index)"
>
- <gl-icon class="gl-mr-0! gl-display-none gl-display-md-block" name="clear" />
- <span class="gl-display-md-none">{{ s__('CiVariables|Remove variable') }}</span>
+ <gl-icon class="gl-mr-0! gl-display-none gl-md-display-block" name="clear" />
+ <span class="gl-md-display-none">{{ s__('CiVariables|Remove variable') }}</span>
</gl-button>
<gl-button
v-else
- class="gl-md-ml-3 gl-mb-3 gl-display-none gl-display-md-block gl-visibility-hidden"
+ class="gl-md-ml-3 gl-mb-3 gl-display-none gl-md-display-block gl-visibility-hidden"
icon="clear"
/>
</template>
@@ -468,6 +481,8 @@ export default {
variant="success"
class="js-no-auto-disable"
data-qa-selector="run_pipeline_button"
+ data-testid="run_pipeline_button"
+ :disabled="submitted"
>{{ s__('Pipeline|Run Pipeline') }}</gl-button
>
<gl-button :href="pipelinesPath">{{ __('Cancel') }}</gl-button>
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
index 2482af2c7f0..e44dedfe2ee 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag.vue
@@ -1,14 +1,14 @@
<script>
import { GlAlert, GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import { __ } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
+import { __ } from '~/locale';
+import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants';
import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql';
-import DagGraph from './dag_graph.vue';
-import DagAnnotations from './dag_annotations.vue';
-import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
import { parseData } from '../parsing_utils';
-import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants';
+import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
+import DagAnnotations from './dag_annotations.vue';
+import DagGraph from './dag_graph.vue';
export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
index 5ba0604fa01..7646c11773c 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
@@ -1,7 +1,10 @@
<script>
import * as d3 from 'd3';
import { uniqueId } from 'lodash';
+import { PARSE_FAILURE } from '../../constants';
+import { getMaxNodes, removeOrphanNodes } from '../parsing_utils';
import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
+import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils';
import {
currentIsLive,
getLiveLinksAsDict,
@@ -10,9 +13,6 @@ import {
toggleLinkHighlight,
togglePathHighlights,
} from './interactions';
-import { getMaxNodes, removeOrphanNodes } from '../parsing_utils';
-import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils';
-import { PARSE_FAILURE } from '../../constants';
export default {
viewOptions: {
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 0ce94d4f02f..1df693704d4 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,9 +1,10 @@
<script>
import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import { reportToSentry } from './utils';
/**
@@ -62,7 +63,7 @@ export default {
*
*/
onClickAction() {
- this.$root.$emit('bv::hide::tooltip', `js-ci-action-${this.link}`);
+ this.$root.$emit(BV_HIDE_TOOLTIP, `js-ci-action-${this.link}`);
this.isDisabled = true;
this.isLoading = true;
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index 6f0deccfef6..caa269f5095 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -2,5 +2,11 @@ export const DOWNSTREAM = 'downstream';
export const MAIN = 'main';
export const UPSTREAM = 'upstream';
+/*
+ this value is based on the gl-pipeline-job-width class
+ plus some extra for the margins
+*/
+export const ONE_COL_WIDTH = 180;
+
export const REST = 'rest';
export const GRAPHQL = 'graphql';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index cd403757fe6..93156d5d05b 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,9 +1,9 @@
<script>
import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
import LinksLayer from '../graph_shared/links_layer.vue';
+import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH } from './constants';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue';
-import { DOWNSTREAM, MAIN, UPSTREAM } from './constants';
import { reportToSentry } from './utils';
export default {
@@ -86,11 +86,11 @@ export default {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
mounted() {
- this.measurements = this.getMeasurements();
+ this.getMeasurements();
},
methods: {
getMeasurements() {
- return {
+ this.measurements = {
width: this.$refs[this.containerId].scrollWidth,
height: this.$refs[this.containerId].scrollHeight,
};
@@ -101,6 +101,13 @@ export default {
setJob(jobName) {
this.hoveredJobName = jobName;
},
+ slidePipelineContainer() {
+ this.$refs.mainPipelineContainer.scrollBy({
+ left: ONE_COL_WIDTH,
+ top: 0,
+ behavior: 'smooth',
+ });
+ },
togglePipelineExpanded(jobName, expanded) {
this.pipelineExpanded = {
expanded,
@@ -116,8 +123,9 @@ export default {
<template>
<div class="js-pipeline-graph">
<div
- class="gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
- :class="{ 'gl-pipeline-min-h gl-py-5': !isLinkedPipeline }"
+ ref="mainPipelineContainer"
+ class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
+ :class="{ 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline }"
>
<linked-graph-wrapper>
<template #upstream>
@@ -153,6 +161,7 @@ export default {
:pipeline-id="pipeline.id"
@refreshPipelineGraph="$emit('refreshPipelineGraph')"
@jobHover="setJob"
+ @updateMeasurements="getMeasurements"
/>
</links-layer>
</div>
@@ -160,11 +169,13 @@ export default {
<template #downstream>
<linked-pipelines-column
v-if="showDownstreamPipelines"
+ class="gl-mr-6"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
:type="$options.pipelineTypeConstants.DOWNSTREAM"
@downstreamHovered="setJob"
@pipelineExpandToggle="togglePipelineExpanded"
+ @scrollContainer="slidePipelineContainer"
@error="onError"
/>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
index 2164dbf4d55..abbf8df6eed 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
@@ -1,10 +1,10 @@
<script>
-import { escape, capitalize } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
-import StageColumnComponentLegacy from './stage_column_component_legacy.vue';
-import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue';
+import { escape, capitalize } from 'lodash';
import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
import { UPSTREAM, DOWNSTREAM, MAIN } from './constants';
+import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue';
+import StageColumnComponentLegacy from './stage_column_component_legacy.vue';
import { reportToSentry } from './utils';
export default {
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 08d6162aeb8..f6aee8c5fcf 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -60,7 +60,7 @@ export default {
>
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
<span class="gl-display-flex gl-align-items-center gl-min-w-0">
- <ci-icon :status="group.status" :size="24" />
+ <ci-icon :status="group.status" :size="24" class="gl-line-height-0" />
<span class="gl-text-truncate mw-70p gl-pl-3">
{{ group.name }}
</span>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 8262d728a24..46ef0457d40 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -1,11 +1,12 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import ActionComponent from './action_component.vue';
-import JobNameComponent from './job_name_component.vue';
-import { sprintf } from '~/locale';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import { sprintf } from '~/locale';
import { accessValue } from './accessors';
+import ActionComponent from './action_component.vue';
import { REST } from './constants';
+import JobNameComponent from './job_name_component.vue';
import { reportToSentry } from './utils';
/**
@@ -144,7 +145,7 @@ export default {
},
methods: {
hideTooltips() {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
index 23a38fc053e..fffd8e1818a 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -30,7 +30,7 @@ export default {
</script>
<template>
<span class="ci-job-name-component mw-100 gl-display-flex gl-align-items-center">
- <ci-icon :size="iconSize" :status="status" />
+ <ci-icon :size="iconSize" :status="status" class="gl-line-height-0" />
<span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
{{ name }}
</span>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index d18e604f087..add7b3445f7 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -1,7 +1,8 @@
<script>
import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui';
-import CiStatus from '~/vue_shared/components/ci_icon.vue';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
+import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { accessValue } from './accessors';
import { DOWNSTREAM, REST, UPSTREAM } from './constants';
import { reportToSentry } from './utils';
@@ -126,7 +127,7 @@ export default {
this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded);
},
hideTooltips() {
- this.$root.$emit('bv::hide::tooltip');
+ this.$root.$emit(BV_HIDE_TOOLTIP);
},
onDownstreamHovered() {
this.$emit('downstreamHovered', this.sourceJobName);
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index 40e6a01b88c..3ce77a1c60a 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -1,8 +1,8 @@
<script>
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
-import LinkedPipeline from './linked_pipeline.vue';
import { LOAD_FAILURE } from '../../constants';
-import { UPSTREAM } from './constants';
+import { ONE_COL_WIDTH, UPSTREAM } from './constants';
+import LinkedPipeline from './linked_pipeline.vue';
import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils';
export default {
@@ -39,6 +39,7 @@ export default {
'gl-pl-3',
'gl-mb-5',
],
+ minWidth: `${ONE_COL_WIDTH}px`,
computed: {
columnClass() {
const positionValues = {
@@ -47,12 +48,6 @@ export default {
};
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
},
- graphPosition() {
- return this.isUpstream ? 'left' : 'right';
- },
- isUpstream() {
- return this.type === UPSTREAM;
- },
computedTitleClasses() {
const positionalClasses = this.isUpstream
? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding']
@@ -60,6 +55,15 @@ export default {
return [...this.$options.titleClasses, ...positionalClasses];
},
+ graphPosition() {
+ return this.isUpstream ? 'left' : 'right';
+ },
+ isUpstream() {
+ return this.type === UPSTREAM;
+ },
+ minWidth() {
+ return this.isUpstream ? 0 : this.$options.minWidth;
+ },
},
methods: {
getPipelineData(pipeline) {
@@ -79,6 +83,7 @@ export default {
},
result() {
this.loadingPipelineId = null;
+ this.$emit('scrollContainer');
},
error(err, _vm, _key, type) {
this.$emit('error', LOAD_FAILURE);
@@ -130,6 +135,9 @@ export default {
this.$emit('pipelineExpandToggle', jobName, expanded);
},
+ showContainer(id) {
+ return this.isExpanded(id) || this.isLoadingPipeline(id);
+ },
},
};
</script>
@@ -158,9 +166,13 @@ export default {
@pipelineClicked="onPipelineClick(pipeline)"
@pipelineExpandToggle="onPipelineExpandToggle"
/>
- <div v-if="isExpanded(pipeline.id)" class="gl-display-inline-block">
+ <div
+ v-if="showContainer(pipeline.id)"
+ :style="{ minWidth }"
+ class="gl-display-inline-block"
+ >
<pipeline-graph
- v-if="currentPipeline"
+ v-if="isExpanded(pipeline.id)"
:type="type"
class="d-inline-block gl-mt-n2"
:pipeline="currentPipeline"
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
index 2f1390e07d1..0d1ff94c275 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
@@ -1,6 +1,6 @@
<script>
-import LinkedPipeline from './linked_pipeline.vue';
import { UPSTREAM } from './constants';
+import LinkedPipeline from './linked_pipeline.vue';
import { reportToSentry } from './utils';
export default {
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 65f8c231885..0a762563114 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -1,11 +1,11 @@
<script>
import { capitalize, escape, isEmpty } from 'lodash';
import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
-import JobItem from './job_item.vue';
-import JobGroupDropdown from './job_group_dropdown.vue';
+import { accessValue } from './accessors';
import ActionComponent from './action_component.vue';
import { GRAPHQL } from './constants';
-import { accessValue } from './accessors';
+import JobGroupDropdown from './job_group_dropdown.vue';
+import JobItem from './job_item.vue';
import { reportToSentry } from './utils';
export default {
@@ -67,6 +67,9 @@ export default {
errorCaptured(err, _vm, info) {
reportToSentry('stage_column_component', `error: ${err}, info: ${info}`);
},
+ mounted() {
+ this.$emit('updateMeasurements');
+ },
methods: {
getGroupId(group) {
return accessValue(GRAPHQL, 'groupId', group);
@@ -75,17 +78,13 @@ export default {
return `ci-badge-${escape(group.name)}`;
},
isFadedOut(jobName) {
- return (
- this.jobHovered &&
- this.highlightedJobs.length > 1 &&
- !this.highlightedJobs.includes(jobName)
- );
+ return this.highlightedJobs.length > 1 && !this.highlightedJobs.includes(jobName);
},
},
};
</script>
<template>
- <main-graph-wrapper class="gl-px-6">
+ <main-graph-wrapper class="gl-px-6" data-testid="stage-column">
<template #stages>
<div
data-testid="stage-column-title"
@@ -123,12 +122,9 @@ export default {
:class="{ 'gl-opacity-3': isFadedOut(group.name) }"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
/>
- <job-group-dropdown
- v-else
- :group="group"
- :pipeline-id="pipelineId"
- :class="{ 'gl-opacity-3': isFadedOut(group.name) }"
- />
+ <div v-else :class="{ 'gl-opacity-3': isFadedOut(group.name) }">
+ <job-group-dropdown :group="group" :pipeline-id="pipelineId" />
+ </div>
</div>
</template>
</main-graph-wrapper>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
index 059e8f9f8db..2cee2fbbd8f 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
@@ -1,9 +1,9 @@
<script>
import { isEmpty, escape } from 'lodash';
import stageColumnMixin from '../../mixins/stage_column_mixin';
-import JobItem from './job_item.vue';
-import JobGroupDropdown from './job_group_dropdown.vue';
import ActionComponent from './action_component.vue';
+import JobGroupDropdown from './job_group_dropdown.vue';
+import JobItem from './job_item.vue';
import { reportToSentry } from './utils';
export default {
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
index 65c215be794..202498fb188 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
@@ -40,10 +40,10 @@ export const generateLinksData = ({ links }, containerID, modifier = '') => {
// positioned in the center of the job node by adding half the height
// of the job pill.
const paddingLeft = parseFloat(
- window.getComputedStyle(containerEl, null).getPropertyValue('padding-left'),
+ window.getComputedStyle(containerEl, null).getPropertyValue('padding-left') || 0,
);
const paddingTop = parseFloat(
- window.getComputedStyle(containerEl, null).getPropertyValue('padding-top'),
+ window.getComputedStyle(containerEl, null).getPropertyValue('padding-top') || 0,
);
const sourceNodeX = sourceNodeCoordinates.right - containerCoordinates.x - paddingLeft;
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
index 89444076ae0..289e04e02c5 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
@@ -118,22 +118,20 @@ export default {
<div class="gl-display-flex gl-relative">
<svg
id="link-svg"
- class="gl-absolute"
+ class="gl-absolute gl-pointer-events-none"
:viewBox="viewBox"
:width="`${containerMeasurements.width}px`"
:height="`${containerMeasurements.height}px`"
>
- <template>
- <path
- v-for="link in links"
- :key="link.path"
- :ref="link.ref"
- :d="link.path"
- class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
- :class="getLinkClasses(link)"
- :stroke-width="$options.STROKE_WIDTH"
- />
- </template>
+ <path
+ v-for="link in links"
+ :key="link.path"
+ :ref="link.ref"
+ :d="link.path"
+ class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
+ :class="getLinkClasses(link)"
+ :stroke-width="$options.STROKE_WIDTH"
+ />
</svg>
<slot></slot>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
index 0993892a574..1c1bc7ecb2a 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
@@ -74,13 +74,15 @@ export default {
<div v-else>
<gl-alert
v-if="showAlert"
- class="gl-w-max-content gl-ml-4"
+ class="gl-ml-4 gl-mb-4"
:primary-button-text="$options.i18n.showLinksAnyways"
@primaryAction="overrideShowLinks"
@dismiss="dismissAlert"
>
{{ $options.i18n.tooManyJobs }}
</gl-alert>
- <slot></slot>
+ <div class="gl-display-flex gl-relative">
+ <slot></slot>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index a20bd70e90a..4ce43b92c93 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -1,13 +1,13 @@
<script>
import { GlAlert, GlButton, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
+import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import ciHeader from '~/vue_shared/components/header_ci_component.vue';
-import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
-import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql';
+import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
+import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql';
import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
-import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql';
-import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
+import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql';
const DELETE_MODAL_ID = 'pipeline-delete-modal';
const POLL_INTERVAL = 10000;
diff --git a/app/assets/javascripts/pipelines/components/legacy_header_component.vue b/app/assets/javascripts/pipelines/components/legacy_header_component.vue
deleted file mode 100644
index c7b72be36ad..00000000000
--- a/app/assets/javascripts/pipelines/components/legacy_header_component.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<script>
-import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
-import ciHeader from '~/vue_shared/components/header_ci_component.vue';
-import eventHub from '../event_hub';
-import { __ } from '~/locale';
-
-const DELETE_MODAL_ID = 'pipeline-delete-modal';
-
-export default {
- name: 'PipelineHeaderSection',
- components: {
- ciHeader,
- GlLoadingIcon,
- GlModal,
- GlButton,
- },
- directives: {
- GlModal: GlModalDirective,
- },
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- isCanceling: false,
- isRetrying: false,
- isDeleting: false,
- };
- },
-
- computed: {
- status() {
- return this.pipeline.details && this.pipeline.details.status;
- },
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.pipeline).length;
- },
- deleteModalConfirmationText() {
- return __(
- 'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.',
- );
- },
- },
-
- methods: {
- cancelPipeline() {
- this.isCanceling = true;
- eventHub.$emit('headerPostAction', this.pipeline.cancel_path);
- },
- retryPipeline() {
- this.isRetrying = true;
- eventHub.$emit('headerPostAction', this.pipeline.retry_path);
- },
- deletePipeline() {
- this.isDeleting = true;
- eventHub.$emit('headerDeleteAction', this.pipeline.delete_path);
- },
- },
- DELETE_MODAL_ID,
-};
-</script>
-<template>
- <div class="pipeline-header-container">
- <ci-header
- v-if="shouldRenderContent"
- :status="status"
- :item-id="pipeline.id"
- :time="pipeline.created_at"
- :user="pipeline.user"
- item-name="Pipeline"
- >
- <gl-button
- v-if="pipeline.retry_path"
- :loading="isRetrying"
- :disabled="isRetrying"
- data-testid="retryButton"
- category="secondary"
- variant="info"
- @click="retryPipeline()"
- >
- {{ __('Retry') }}
- </gl-button>
-
- <gl-button
- v-if="pipeline.cancel_path"
- :loading="isCanceling"
- :disabled="isCanceling"
- data-testid="cancelPipeline"
- class="gl-ml-3"
- category="primary"
- variant="danger"
- @click="cancelPipeline()"
- >
- {{ __('Cancel running') }}
- </gl-button>
-
- <gl-button
- v-if="pipeline.delete_path"
- v-gl-modal="$options.DELETE_MODAL_ID"
- :loading="isDeleting"
- :disabled="isDeleting"
- data-testid="deletePipeline"
- class="gl-ml-3"
- category="secondary"
- variant="danger"
- >
- {{ __('Delete') }}
- </gl-button>
- </ci-header>
-
- <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3" />
-
- <gl-modal
- :modal-id="$options.DELETE_MODAL_ID"
- :title="__('Delete pipeline')"
- :ok-title="__('Delete pipeline')"
- ok-variant="danger"
- @ok="deletePipeline()"
- >
- <p>
- {{ deleteModalConfirmationText }}
- </p>
- </gl-modal>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
index 8636808b69e..4a7ee3b2af7 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -1,13 +1,13 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
+import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
+import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants';
+import { createJobsHash, generateJobNeedsDict } from '../../utils';
import { generateLinksData } from '../graph_shared/drawing_utils';
+import { parseData } from '../parsing_utils';
import JobPill from './job_pill.vue';
import StagePill from './stage_pill.vue';
-import { parseData } from '../parsing_utils';
-import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants';
-import { createJobsHash, generateJobNeedsDict } from '../../utils';
-import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
export default {
components: {
@@ -224,17 +224,15 @@ export default {
data-testid="graph-container"
>
<svg :viewBox="viewBox" :width="width" :height="height" class="gl-absolute">
- <template>
- <path
- v-for="link in links"
- :key="link.path"
- :ref="link.ref"
- :d="link.path"
- class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
- :class="getLinkClasses(link)"
- :stroke-width="$options.STROKE_WIDTH"
- />
- </template>
+ <path
+ v-for="link in links"
+ :key="link.path"
+ :ref="link.ref"
+ :d="link.path"
+ class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
+ :class="getLinkClasses(link)"
+ :stroke-width="$options.STROKE_WIDTH"
+ />
</svg>
<div
v-for="(stage, index) in pipelineStages"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
index ee26ea2f007..8a656bb47f4 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
@@ -1,24 +1,13 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { isExperimentEnabled } from '~/lib/utils/experimentation';
import { s__ } from '~/locale';
-import Tracking from '~/tracking';
export default {
i18n: {
- control: {
- infoMessage: s__(`Pipelines|Continuous Integration can help
- catch bugs by running your tests automatically,
- while Continuous Deployment can help you deliver
- code to your product environment.`),
- buttonMessage: s__('Pipelines|Get started with Pipelines'),
- },
- experiment: {
- infoMessage: s__(`Pipelines|GitLab CI/CD can automatically build,
+ infoMessage: s__(`Pipelines|GitLab CI/CD can automatically build,
test, and deploy your code. Let GitLab take care of time
consuming tasks, so you can spend more time creating.`),
- buttonMessage: s__('Pipelines|Get started with CI/CD'),
- },
+ buttonMessage: s__('Pipelines|Get started with CI/CD'),
},
name: 'PipelinesEmptyState',
components: {
@@ -38,23 +27,6 @@ export default {
required: true,
},
},
- mounted() {
- this.track('viewed');
- },
- methods: {
- track(action) {
- if (!gon.tracking_data) {
- return;
- }
-
- const { category, value, label, property } = gon.tracking_data;
-
- Tracking.event(category, action, { value, label, property });
- },
- isExperimentEnabled() {
- return isExperimentEnabled('pipelinesEmptyState');
- },
- },
};
</script>
<template>
@@ -70,11 +42,7 @@ export default {
{{ s__('Pipelines|Build with confidence') }}
</h4>
<p data-testid="info-text">
- {{
- isExperimentEnabled()
- ? $options.i18n.experiment.infoMessage
- : $options.i18n.control.infoMessage
- }}
+ {{ $options.i18n.infoMessage }}
</p>
<div class="gl-text-center">
@@ -83,13 +51,8 @@ export default {
variant="info"
category="primary"
data-testid="get-started-pipelines"
- @click="track('documentation_clicked')"
>
- {{
- isExperimentEnabled()
- ? $options.i18n.experiment.buttonMessage
- : $options.i18n.control.buttonMessage
- }}
+ {{ $options.i18n.buttonMessage }}
</gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
index 1569b326b31..24b5c85c9d6 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
@@ -1,9 +1,9 @@
<script>
/* eslint-disable vue/no-v-html */
-import { isEmpty } from 'lodash';
import { GlLink, GlModal } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import { isEmpty } from 'lodash';
import { __, s__, sprintf } from '~/locale';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
/**
* Pipeline Stop Modal.
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
index d1bac078642..823ada133d2 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
@@ -77,6 +77,15 @@ export default {
>{{ __('latest') }}</gl-badge
>
<gl-badge
+ v-if="pipeline.flags.merge_train_pipeline"
+ v-gl-tooltip
+ :title="__('This is a merge train pipeline')"
+ variant="info"
+ size="sm"
+ data-testid="pipeline-url-train"
+ >{{ __('train') }}</gl-badge
+ >
+ <gl-badge
v-if="pipeline.flags.yaml_errors"
v-gl-tooltip
:title="pipeline.yaml_errors"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index ec7c5764be1..48009a9fcb8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -1,28 +1,34 @@
<script>
+import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { isEqual } from 'lodash';
-import { GlIcon } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import PipelinesService from '../../services/pipelines_service';
-import pipelinesMixin from '../../mixins/pipelines';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import { __, s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
+import PipelinesMixin from '../../mixins/pipelines_mixin';
+import PipelinesService from '../../services/pipelines_service';
+import { validateParams } from '../../utils';
+import SvgBlankState from './blank_state.vue';
+import EmptyState from './empty_state.vue';
import NavigationControls from './nav_controls.vue';
-import { getParameterByName } from '~/lib/utils/common_utils';
-import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
-import { validateParams } from '../../utils';
-import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
+import PipelinesTableComponent from './pipelines_table.vue';
export default {
components: {
- TablePagination,
+ EmptyState,
+ GlIcon,
+ GlLoadingIcon,
NavigationTabs,
NavigationControls,
PipelinesFilteredSearch,
- GlIcon,
+ PipelinesTableComponent,
+ SvgBlankState,
+ TablePagination,
},
- mixins: [pipelinesMixin, CIPaginationMixin],
+ mixins: [PipelinesMixin],
props: {
store: {
type: Object,
@@ -217,6 +223,20 @@ export default {
this.requestData = { page: this.page, scope: this.scope, ...this.validatedParams };
},
methods: {
+ onChangeTab(scope) {
+ if (this.scope === scope) {
+ return;
+ }
+
+ let params = {
+ scope,
+ page: '1',
+ };
+
+ params = this.onChangeWithFilter(params);
+
+ this.updateContent(params);
+ },
successCallback(resp) {
// Because we are polling & the user is interacting verify if the response received
// matches the last request made
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
index 1ea71610897..6890cbb9bed 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
@@ -1,7 +1,7 @@
<script>
-import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from '~/flash';
import { s__, __, sprintf } from '~/locale';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import eventHub from '../../event_hub';
@@ -11,10 +11,10 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- GlIcon,
GlCountdown,
- GlButton,
- GlLoadingIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
},
props: {
actions: {
@@ -61,7 +61,7 @@ export default {
})
.catch(() => {
this.isLoading = false;
- flash(__('An error occurred while making the request.'));
+ createFlash({ message: __('An error occurred while making the request.') });
});
},
@@ -76,39 +76,27 @@ export default {
};
</script>
<template>
- <div class="btn-group">
- <button
- v-gl-tooltip
- type="button"
- :disabled="isLoading"
- class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
- :title="__('Run manual or delayed jobs')"
- data-toggle="dropdown"
- :aria-label="__('Run manual or delayed jobs')"
+ <gl-dropdown
+ v-gl-tooltip
+ :title="__('Run manual or delayed jobs')"
+ :loading="isLoading"
+ data-testid="pipelines-manual-actions-dropdown"
+ right
+ icon="play"
+ >
+ <gl-dropdown-item
+ v-for="action in actions"
+ :key="action.path"
+ :disabled="isActionDisabled(action)"
+ @click="onClickAction(action)"
>
- <gl-icon name="play" class="icon-play" />
- <gl-icon name="chevron-down" />
- <gl-loading-icon v-if="isLoading" />
- </button>
-
- <ul class="dropdown-menu dropdown-menu-right">
- <li v-for="action in actions" :key="action.path">
- <gl-button
- category="tertiary"
- :class="{ disabled: isActionDisabled(action) }"
- :disabled="isActionDisabled(action)"
- class="js-pipeline-action-link"
- @click="onClickAction(action)"
- >
- <div class="d-flex justify-content-between flex-wrap">
- {{ action.name }}
- <span v-if="action.scheduled_at">
- <gl-icon name="clock" />
- <gl-countdown :end-date-string="action.scheduled_at" />
- </span>
- </div>
- </gl-button>
- </li>
- </ul>
- </div>
+ <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
+ {{ action.name }}
+ <span v-if="action.scheduled_at">
+ <gl-icon name="clock" />
+ <gl-countdown :end-date-string="action.scheduled_at" />
+ </span>
+ </div>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
index 127503f1307..492c562ec5c 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
@@ -2,10 +2,10 @@
import { GlFilteredSearch } from '@gitlab/ui';
import { map } from 'lodash';
import { __, s__ } from '~/locale';
-import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
import PipelineStatusToken from './tokens/pipeline_status_token.vue';
import PipelineTagNameToken from './tokens/pipeline_tag_name_token.vue';
+import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
export default {
userType: 'username',
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index 6c60594efca..24c67184e56 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -1,8 +1,8 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
-import PipelinesTableRowComponent from './pipelines_table_row.vue';
-import PipelineStopModal from './pipeline_stop_modal.vue';
import eventHub from '../../event_hub';
+import PipelineStopModal from './pipeline_stop_modal.vue';
+import PipelinesTableRowComponent from './pipelines_table_row.vue';
/**
* Pipelines Table Component.
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
index b6c4e617a90..572abe2a24a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
@@ -1,22 +1,17 @@
<script>
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
-import eventHub from '../../event_hub';
import { __ } from '~/locale';
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CommitComponent from '~/vue_shared/components/commit.vue';
+import { PIPELINES_TABLE } from '../../constants';
+import eventHub from '../../event_hub';
+import PipelineTriggerer from './pipeline_triggerer.vue';
+import PipelineUrl from './pipeline_url.vue';
import PipelinesActionsComponent from './pipelines_actions.vue';
import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import PipelineStage from './stage.vue';
-import PipelineUrl from './pipeline_url.vue';
-import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelinesTimeago from './time_ago.vue';
-import CommitComponent from '~/vue_shared/components/commit.vue';
-import { PIPELINES_TABLE } from '../../constants';
-/**
- * Pipeline table row.
- *
- * Given the received object renders a table row in the pipelines' table.
- */
export default {
i18n: {
cancelTitle: __('Cancel'),
@@ -127,116 +122,30 @@ export default {
return commitAuthorInformation;
},
-
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
commitTag() {
- if (this.pipeline.ref && this.pipeline.ref.tag) {
- return this.pipeline.ref.tag;
- }
- return undefined;
+ return this.pipeline?.ref?.tag;
},
-
- /**
- * If provided, returns the commit ref.
- * Needed to render the commit component column.
- *
- * Matches `path` prop sent in the API to `ref_url` prop needed
- * in the commit component.
- *
- * @returns {Object|Undefined}
- */
commitRef() {
- if (this.pipeline.ref) {
- return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
- if (prop === 'path') {
- accumulator.ref_url = this.pipeline.ref[prop];
- } else {
- accumulator[prop] = this.pipeline.ref[prop];
- }
- return accumulator;
- }, {});
- }
-
- return undefined;
+ return this.pipeline?.ref;
},
-
- /**
- * If provided, returns the commit url.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
commitUrl() {
- if (this.pipeline.commit && this.pipeline.commit.commit_path) {
- return this.pipeline.commit.commit_path;
- }
- return undefined;
+ return this.pipeline?.commit?.commit_path;
},
-
- /**
- * If provided, returns the commit short sha.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
commitShortSha() {
- if (this.pipeline.commit && this.pipeline.commit.short_id) {
- return this.pipeline.commit.short_id;
- }
- return undefined;
+ return this.pipeline?.commit?.short_id;
},
-
- /**
- * If provided, returns the commit title.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
commitTitle() {
- if (this.pipeline.commit && this.pipeline.commit.title) {
- return this.pipeline.commit.title;
- }
- return undefined;
+ return this.pipeline?.commit?.title;
},
-
- /**
- * Timeago components expects a number
- *
- * @return {type} description
- */
pipelineDuration() {
- if (this.pipeline.details && this.pipeline.details.duration) {
- return this.pipeline.details.duration;
- }
-
- return 0;
+ return this.pipeline?.details?.duration ?? 0;
},
-
- /**
- * Timeago component expects a String.
- *
- * @return {String}
- */
pipelineFinishedAt() {
- if (this.pipeline.details && this.pipeline.details.finished_at) {
- return this.pipeline.details.finished_at;
- }
-
- return '';
+ return this.pipeline?.details?.finished_at ?? '';
},
-
pipelineStatus() {
- if (this.pipeline.details && this.pipeline.details.status) {
- return this.pipeline.details.status;
- }
- return {};
+ return this.pipeline?.details?.status ?? {};
},
-
displayPipelineActions() {
return (
this.pipeline.flags.retryable ||
@@ -245,11 +154,9 @@ export default {
this.pipeline.details.artifacts.length
);
},
-
isChildView() {
return this.viewType === 'child';
},
-
isCancelling() {
return this.cancelingPipeline === this.pipeline.id;
},
@@ -355,7 +262,7 @@ export default {
:title="$options.i18n.redeployTitle"
:disabled="isRetrying"
:loading="isRetrying"
- class="js-pipelines-retry-button btn-retry"
+ class="js-pipelines-retry-button"
data-qa-selector="pipeline_retry_button"
icon="repeat"
variant="default"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
index a9154d93194..f5dfb9e72d5 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
@@ -11,27 +11,27 @@
* 3. Merge request widget
* 4. Commit widget
*/
-
+import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import $ from 'jquery';
-import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { PIPELINES_TABLE } from '../../constants';
import eventHub from '../../event_hub';
import JobItem from '../graph/job_item.vue';
-import { PIPELINES_TABLE } from '../../constants';
export default {
components: {
GlIcon,
- JobItem,
GlLoadingIcon,
+ GlDropdown,
+ JobItem,
},
-
directives: {
GlTooltip: GlTooltipDirective,
},
-
+ mixins: [glFeatureFlagsMixin()],
props: {
stage: {
type: Object,
@@ -50,30 +50,25 @@ export default {
default: '',
},
},
-
data() {
return {
isLoading: false,
- dropdownContent: '',
+ dropdownContent: [],
};
},
-
computed: {
- dropdownClass() {
- return this.dropdownContent.length > 0
- ? 'js-builds-dropdown-container'
- : 'js-builds-dropdown-loading';
+ isCiMiniPipelineGlDropdown() {
+ // Feature flag ci_mini_pipeline_gl_dropdown
+ // See more at https://gitlab.com/gitlab-org/gitlab/-/issues/300400
+ return this.glFeatures?.ciMiniPipelineGlDropdown;
},
-
triggerButtonClass() {
return `ci-status-icon-${this.stage.status.group}`;
},
-
borderlessIcon() {
return `${this.stage.status.icon}_borderless`;
},
},
-
watch: {
updateDropdown() {
if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
@@ -81,14 +76,17 @@ export default {
}
},
},
-
updated() {
- if (this.dropdownContent.length > 0) {
+ if (!this.isCiMiniPipelineGlDropdown && this.dropdownContent.length) {
this.stopDropdownClickPropagation();
}
},
-
methods: {
+ onShowDropdown() {
+ eventHub.$emit('clickedDropdown');
+ this.isLoading = true;
+ this.fetchJobs();
+ },
onClickStage() {
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
@@ -96,7 +94,6 @@ export default {
this.fetchJobs();
}
},
-
fetchJobs() {
axios
.get(this.stage.dropdown_path)
@@ -105,13 +102,16 @@ export default {
this.isLoading = false;
})
.catch(() => {
- this.closeDropdown();
+ if (this.isCiMiniPipelineGlDropdown) {
+ this.$refs.stageGlDropdown.hide();
+ } else {
+ this.closeDropdown();
+ }
this.isLoading = false;
Flash(__('Something went wrong on our end.'));
});
},
-
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
@@ -119,6 +119,8 @@ export default {
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
+ *
+ * Note: This should be removed once ci_mini_pipeline_gl_dropdown FF is removed as true.
*/
stopDropdownClickPropagation() {
$(
@@ -128,23 +130,24 @@ export default {
e.stopPropagation();
});
},
-
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
-
isDropdownOpen() {
return this.$el.classList.contains('show');
},
-
pipelineActionRequestComplete() {
if (this.type === PIPELINES_TABLE) {
// warn the table to update
eventHub.$emit('refreshPipelinesTable');
+ return;
+ }
+ // close the dropdown in mr widget
+ if (this.isCiMiniPipelineGlDropdown) {
+ this.$refs.stageGlDropdown.hide();
} else {
- // close the dropdown in mr widget
$(this.$refs.dropdown).dropdown('toggle');
}
},
@@ -154,31 +157,30 @@ export default {
<template>
<div class="dropdown">
- <button
- id="stageDropdown"
- ref="dropdown"
+ <gl-dropdown
+ v-if="isCiMiniPipelineGlDropdown"
+ ref="stageGlDropdown"
v-gl-tooltip.hover
- :class="triggerButtonClass"
+ data-testid="mini-pipeline-graph-dropdown"
:title="stage.title"
- class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
- data-toggle="dropdown"
- data-display="static"
- type="button"
- aria-haspopup="true"
- aria-expanded="false"
- @click="onClickStage"
- >
- <span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none">
- <gl-icon :name="borderlessIcon" />
- </span>
- </button>
-
- <div
- class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
- aria-labelledby="stageDropdown"
+ variant="link"
+ :lazy="true"
+ :popper-opts="{ placement: 'bottom' }"
+ :toggle-class="['mini-pipeline-graph-gl-dropdown-toggle', triggerButtonClass]"
+ menu-class="mini-pipeline-graph-dropdown-menu"
+ @show="onShowDropdown"
>
+ <template #button-content>
+ <span class="gl-pointer-events-none">
+ <gl-icon :name="borderlessIcon" />
+ </span>
+ </template>
<gl-loading-icon v-if="isLoading" />
- <ul v-else class="js-builds-dropdown-list scrollable-menu">
+ <ul
+ v-else
+ class="js-builds-dropdown-list scrollable-menu"
+ data-testid="mini-pipeline-graph-dropdown-menu-list"
+ >
<li v-for="job in dropdownContent" :key="job.id">
<job-item
:dropdown-length="dropdownContent.length"
@@ -188,6 +190,45 @@ export default {
/>
</li>
</ul>
- </div>
+ </gl-dropdown>
+
+ <template v-else>
+ <button
+ id="stageDropdown"
+ ref="dropdown"
+ v-gl-tooltip.hover
+ :class="triggerButtonClass"
+ :title="stage.title"
+ class="mini-pipeline-graph-dropdown-toggle"
+ data-testid="mini-pipeline-graph-dropdown-toggle"
+ data-toggle="dropdown"
+ data-display="static"
+ type="button"
+ aria-haspopup="true"
+ aria-expanded="false"
+ @click="onClickStage"
+ >
+ <span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none">
+ <gl-icon :name="borderlessIcon" />
+ </span>
+ </button>
+
+ <div
+ class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
+ aria-labelledby="stageDropdown"
+ >
+ <gl-loading-icon v-if="isLoading" />
+ <ul v-else class="js-builds-dropdown-list scrollable-menu">
+ <li v-for="job in dropdownContent" :key="job.id">
+ <job-item
+ :dropdown-length="dropdownContent.length"
+ :job="job"
+ css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+ </li>
+ </ul>
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
index 24456574a6f..20a232beb83 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
@@ -2,8 +2,8 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
-import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
index 1241803c612..4a8d89ebe37 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
@@ -2,8 +2,8 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
-import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
index 504cf138d07..2edc84e62cb 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
@@ -1,12 +1,13 @@
<script>
-import { GlModal } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlBadge, GlModal } from '@gitlab/ui';
+import { __, n__, sprintf } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue';
export default {
name: 'TestCaseDetails',
components: {
CodeBlock,
+ GlBadge,
GlModal,
},
props: {
@@ -17,13 +18,37 @@ export default {
testCase: {
type: Object,
required: true,
- validator: ({ classname, formattedTime, name }) =>
- Boolean(classname) && Boolean(formattedTime) && Boolean(name),
+ },
+ },
+ computed: {
+ failureHistoryMessage() {
+ if (!this.hasRecentFailures) {
+ return null;
+ }
+
+ return sprintf(
+ n__(
+ 'Reports|Failed %{count} time in %{baseBranch} in the last 14 days',
+ 'Reports|Failed %{count} times in %{baseBranch} in the last 14 days',
+ this.recentFailures.count,
+ ),
+ {
+ count: this.recentFailures.count,
+ baseBranch: this.recentFailures.base_branch,
+ },
+ );
+ },
+ hasRecentFailures() {
+ return Boolean(this.recentFailures);
+ },
+ recentFailures() {
+ return this.testCase.recent_failures;
},
},
text: {
name: __('Name'),
duration: __('Execution time'),
+ history: __('History'),
trace: __('System output'),
},
modalCloseButton: {
@@ -53,6 +78,13 @@ export default {
</div>
</div>
+ <div v-if="testCase.recent_failures" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
+ <strong class="gl-text-right col-sm-3">{{ $options.text.history }}</strong>
+ <div class="col-sm-9" data-testid="test-case-recent-failures">
+ <gl-badge variant="warning">{{ failureHistoryMessage }}</gl-badge>
+ </div>
+ </div>
+
<div
v-if="testCase.system_output"
class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3"
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
index a56dcf48d92..58d60e2a185 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import { mapActions, mapGetters, mapState } from 'vuex';
import TestSuiteTable from './test_suite_table.vue';
import TestSummary from './test_summary.vue';
import TestSummaryTable from './test_summary_table.vue';
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index 4b4fb6082c6..51373e712ff 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -1,13 +1,14 @@
<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
import {
GlModalDirective,
GlTooltipDirective,
GlFriendlyWrap,
GlIcon,
+ GlLink,
GlButton,
GlPagination,
} from '@gitlab/ui';
+import { mapState, mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
import TestCaseDetails from './test_case_details.vue';
@@ -16,6 +17,7 @@ export default {
components: {
GlIcon,
GlFriendlyWrap,
+ GlLink,
GlButton,
GlPagination,
TestCaseDetails,
@@ -97,8 +99,11 @@ export default {
<div class="table-section section-10 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Filename') }}</div>
<div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break">
- <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.file" />
+ <gl-link v-if="testCase.file" :href="testCase.filePath" target="_blank">
+ <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.file" />
+ </gl-link>
<gl-button
+ v-if="testCase.file"
v-gl-tooltip
size="small"
category="tertiary"
@@ -114,7 +119,7 @@ export default {
<div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div>
<div class="table-mobile-content text-center">
<div
- class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
+ class="ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
:class="`ci-status-icon-${testCase.status}`"
>
<gl-icon :size="24" :name="testCase.icon" />
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
index 5f9c0be3ccc..2b44ce57faa 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
import { s__ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
index 22cdb6b8f72..2321728e30c 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
@@ -1,21 +1,13 @@
import Visibility from 'visibilityjs';
-import { GlLoadingIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
-import EmptyState from '../components/pipelines_list/empty_state.vue';
-import SvgBlankState from '../components/pipelines_list/blank_state.vue';
-import PipelinesTableComponent from '../components/pipelines_list/pipelines_table.vue';
-import eventHub from '../event_hub';
+import { __ } from '~/locale';
+import { validateParams } from '~/pipelines/utils';
import { CANCEL_REQUEST } from '../constants';
+import eventHub from '../event_hub';
export default {
- components: {
- PipelinesTableComponent,
- SvgBlankState,
- EmptyState,
- GlLoadingIcon,
- },
data() {
return {
isLoading: false,
@@ -76,6 +68,25 @@ export default {
this.poll.stop();
},
methods: {
+ updateInternalState(parameters) {
+ this.poll.stop();
+
+ const queryString = Object.keys(parameters)
+ .map((parameter) => {
+ const value = parameters[parameter];
+ // update internal state for UI
+ this[parameter] = value;
+ return `${parameter}=${encodeURIComponent(value)}`;
+ })
+ .join('&');
+
+ // update polling parameters
+ this.requestData = parameters;
+
+ historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
+
+ this.isLoading = true;
+ },
/**
* Handles URL and query parameter changes.
* When the user uses the pagination or the tabs,
@@ -184,5 +195,23 @@ export default {
})
.finally(() => this.store.toggleIsRunningPipeline(false));
},
+ onChangePage(page) {
+ /* URLS parameters are strings, we need to parse to match types */
+ let params = {
+ page: Number(page).toString(),
+ };
+
+ if (this.scope) {
+ params.scope = this.scope;
+ }
+
+ params = this.onChangeWithFilter(params);
+
+ this.updateContent(params);
+ },
+
+ onChangeWithFilter(params) {
+ return { ...params, ...validateParams(this.requestData) };
+ },
},
};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 133608b9801..f837851e5c1 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,16 +1,13 @@
import Vue from 'vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
-import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
-import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
+import Translate from '~/vue_shared/translate';
import PipelineGraphLegacy from './components/graph/graph_component_legacy.vue';
-import createDagApp from './pipeline_details_dag';
-import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
-import legacyPipelineHeader from './components/legacy_header_component.vue';
-import eventHub from './event_hub';
+import { reportToSentry } from './components/graph/utils';
import TestReports from './components/test_reports/test_reports.vue';
+import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
+import createDagApp from './pipeline_details_dag';
import createTestReportsStore from './stores/test_reports';
-import { reportToSentry } from './components/graph/utils';
Vue.use(Translate);
@@ -59,62 +56,11 @@ const createLegacyPipelinesDetailApp = (mediator) => {
});
};
-const createLegacyPipelineHeaderApp = (mediator) => {
- if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) {
- return;
- }
- // eslint-disable-next-line no-new
- new Vue({
- el: SELECTORS.PIPELINE_HEADER,
- components: {
- legacyPipelineHeader,
- },
- data() {
- return {
- mediator,
- };
- },
- created() {
- eventHub.$on('headerPostAction', this.postAction);
- eventHub.$on('headerDeleteAction', this.deleteAction);
- },
- beforeDestroy() {
- eventHub.$off('headerPostAction', this.postAction);
- eventHub.$off('headerDeleteAction', this.deleteAction);
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('pipeline_details_bundle_legacy', `error: ${err}, info: ${info}`);
- },
- methods: {
- postAction(path) {
- this.mediator.service
- .postAction(path)
- .then(() => this.mediator.refreshPipeline())
- .catch(() => Flash(__('An error occurred while making the request.')));
- },
- deleteAction(path) {
- this.mediator.stopPipelinePoll();
- this.mediator.service
- .deleteAction(path)
- .then(({ request }) => redirectTo(setUrlFragment(request.responseURL, 'delete_success')))
- .catch(() => Flash(__('An error occurred while deleting the pipeline.')));
- },
- },
- render(createElement) {
- return createElement('legacy-pipeline-header', {
- props: {
- isLoading: this.mediator.state.isLoading,
- pipeline: this.mediator.store.state.pipeline,
- },
- });
- },
- });
-};
-
const createTestDetails = () => {
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
- const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
+ const { blobPath, summaryEndpoint, suiteEndpoint } = el?.dataset || {};
const testReportsStore = createTestReportsStore({
+ blobPath,
summaryEndpoint,
suiteEndpoint,
});
@@ -132,26 +78,16 @@ const createTestDetails = () => {
});
};
-export default async function () {
+export default async function initPipelineDetailsBundle() {
createTestDetails();
createDagApp();
- const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
- let mediator;
+ const canShowNewPipelineDetails =
+ gon.features.graphqlPipelineDetails || gon.features.graphqlPipelineDetailsUsers;
- if (!gon.features.graphqlPipelineHeader || !gon.features.graphqlPipelineDetails) {
- try {
- const { default: PipelinesMediator } = await import(
- /* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator'
- );
- mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
- mediator.fetchPipeline();
- } catch {
- Flash(__('An error occurred while loading the pipeline.'));
- }
- }
+ const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
- if (gon.features.graphqlPipelineDetails) {
+ if (canShowNewPipelineDetails) {
try {
const { createPipelinesDetailApp } = await import(
/* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph'
@@ -163,19 +99,21 @@ export default async function () {
Flash(__('An error occurred while loading the pipeline.'));
}
} else {
+ const { default: PipelinesMediator } = await import(
+ /* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator'
+ );
+ const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
+ mediator.fetchPipeline();
+
createLegacyPipelinesDetailApp(mediator);
}
- if (gon.features.graphqlPipelineHeader) {
- try {
- const { createPipelineHeaderApp } = await import(
- /* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header'
- );
- createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER);
- } catch {
- Flash(__('An error occurred while loading a section of this page.'));
- }
- } else {
- createLegacyPipelineHeaderApp(mediator);
+ try {
+ const { createPipelineHeaderApp } = await import(
+ /* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header'
+ );
+ createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER);
+ } catch {
+ Flash(__('An error occurred while loading a section of this page.'));
}
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_dag.js b/app/assets/javascripts/pipelines/pipeline_details_dag.js
index d37c72a4f2a..4ee0ad462d2 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_dag.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_dag.js
@@ -12,7 +12,7 @@ const apolloProvider = new VueApollo({
const createDagApp = () => {
const el = document.querySelector('#js-pipeline-dag-vue');
- if (!window.gon?.features?.dagPipelineTab || !el) {
+ if (!el) {
return;
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_graph.js b/app/assets/javascripts/pipelines/pipeline_details_graph.js
index 2d46bb5ec26..55f3731a3ca 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_graph.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_graph.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
import { GRAPHQL } from './components/graph/constants';
+import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
import { reportToSentry } from './components/graph/utils';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index 74c5fc45644..09637c25654 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -1,9 +1,9 @@
import Visibility from 'visibilityjs';
-import PipelineStore from './stores/pipeline_store';
import { deprecatedCreateFlash as Flash } from '../flash';
import Poll from '../lib/utils/poll';
import { __ } from '../locale';
import PipelineService from './services/pipeline_service';
+import PipelineStore from './stores/pipeline_store';
export default class pipelinesMediator {
constructor(options = {}) {
diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js
index 4575a99f60f..7bcc51e18e5 100644
--- a/app/assets/javascripts/pipelines/pipelines_index.js
+++ b/app/assets/javascripts/pipelines/pipelines_index.js
@@ -1,12 +1,12 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import { __ } from '~/locale';
-import { doesHashExistInUrl } from '~/lib/utils/url_utility';
+import Vue from 'vue';
import {
parseBoolean,
historyReplaceState,
buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils';
+import { doesHashExistInUrl } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import Translate from '~/vue_shared/translate';
import Pipelines from './components/pipelines_list/pipelines.vue';
import PipelinesStore from './stores/pipelines_store';
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 0b06bcf243a..523ca13b6c3 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -1,5 +1,5 @@
-import axios from '../../lib/utils/axios_utils';
import Api from '~/api';
+import axios from '../../lib/utils/axios_utils';
import { validateParams } from '../utils';
export default class PipelinesService {
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
index 3c664457756..6de345233ae 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -1,7 +1,7 @@
-import axios from '~/lib/utils/axios_utils';
-import * as types from './mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
+import * as types from './mutation_types';
export const fetchSummary = ({ state, commit, dispatch }) => {
dispatch('toggleLoading');
@@ -28,16 +28,12 @@ export const fetchTestSuite = ({ state, commit, dispatch }, index) => {
dispatch('toggleLoading');
- const { name = '', build_ids = [] } = state.testReports?.test_suites?.[index] || {};
+ const { build_ids = [] } = state.testReports?.test_suites?.[index] || {};
// Replacing `/:suite_name.json` with the name of the suite. Including the extra characters
// to ensure that we replace exactly the template part of the URL string
- const endpoint = state.suiteEndpoint?.replace(
- '/:suite_name.json',
- `/${encodeURIComponent(name)}.json`,
- );
return axios
- .get(endpoint, { params: { build_ids } })
+ .get(state.suiteEndpoint, { params: { build_ids } })
.then(({ data }) => commit(types.SET_SUITE, { suite: data, index }))
.catch(() => {
createFlash(s__('TestReports|There was an error fetching the test suite.'));
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
index c31e7dd114f..03680de0fa9 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
@@ -1,4 +1,4 @@
-import { addIconStatus, formattedTime } from './utils';
+import { addIconStatus, formatFilePath, formattedTime } from './utils';
export const getTestSuites = (state) => {
const { test_suites: testSuites = [] } = state.testReports;
@@ -17,7 +17,15 @@ export const getSuiteTests = (state) => {
const { page, perPage } = state.pageInfo;
const start = (page - 1) * perPage;
- return testCases.map(addIconStatus).slice(start, start + perPage);
+ return testCases
+ .map((testCase) => ({
+ ...testCase,
+ classname: testCase.classname || '',
+ name: testCase.name || '',
+ filePath: testCase.file ? `${state.blobPath}/${formatFilePath(testCase.file)}` : null,
+ }))
+ .map(addIconStatus)
+ .slice(start, start + perPage);
};
export const getSuiteTestCount = (state) => getSelectedSuite(state)?.test_cases?.length || 0;
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/index.js b/app/assets/javascripts/pipelines/stores/test_reports/index.js
index 204dfc2fb01..64d4b8bafb1 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/index.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import state from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
+import state from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js
index 7f5da549a9d..0ee6f53fa58 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/state.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js
@@ -1,4 +1,5 @@
-export default ({ summaryEndpoint = '', suiteEndpoint = '' }) => ({
+export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
+ blobPath,
summaryEndpoint,
suiteEndpoint,
testReports: {},
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
index 5c1f27b166a..63a58798958 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
@@ -1,19 +1,28 @@
import { __, sprintf } from '../../../locale';
import { TestStatus } from '../../constants';
+/**
+ * Removes `./` from the beginning of a file path so it can be appended onto a blob path
+ * @param {String} file
+ * @returns {String} - formatted value
+ */
+export function formatFilePath(file) {
+ return file.replace(/^\.?\/*/, '');
+}
+
export function iconForTestStatus(status) {
switch (status) {
case TestStatus.SUCCESS:
- return 'status_success_borderless';
+ return 'status_success';
case TestStatus.FAILED:
- return 'status_failed_borderless';
+ return 'status_failed';
case TestStatus.ERROR:
- return 'status_warning_borderless';
+ return 'status_warning';
case TestStatus.SKIPPED:
- return 'status_skipped_borderless';
+ return 'status_skipped';
case TestStatus.UNKNOWN:
default:
- return 'status_notfound_borderless';
+ return 'status_notfound';
}
}
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 50bb23b7e63..22820fca43e 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -1,6 +1,6 @@
import { pickBy } from 'lodash';
-import { SUPPORTED_FILTER_PARAMETERS } from './constants';
import { createNodeDict } from './components/parsing_utils';
+import { SUPPORTED_FILTER_PARAMETERS } from './constants';
export const validateParams = (params) => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
@@ -49,10 +49,10 @@ export const generateJobNeedsDict = (jobs = {}) => {
// to the list of `needs` to ensure we can properly reference it.
const group = jobs[job];
if (group.size > 1) {
- return [job, group.name, ...newNeeds];
+ return [job, group.name, newNeeds];
}
- return [job, ...newNeeds];
+ return [job, newNeeds];
})
.flat(Infinity);
};
diff --git a/app/assets/javascripts/popovers/index.js b/app/assets/javascripts/popovers/index.js
index eebbfbdcc68..7db669b8c52 100644
--- a/app/assets/javascripts/popovers/index.js
+++ b/app/assets/javascripts/popovers/index.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import { toArray } from 'lodash';
+import Vue from 'vue';
import PopoversComponent from './components/popovers.vue';
let app;
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index f06dc72d365..2336cb18cb5 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlModal } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
+import { __, s__, sprintf } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index 869fdccc800..c5478aa0226 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -1,9 +1,9 @@
<script>
-import { escape } from 'lodash';
import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { escape } from 'lodash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__, sprintf } from '~/locale';
-import { deprecatedCreateFlash as Flash } from '~/flash';
export default {
components: {
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
index 5e89002b3bc..00fe0bcf89b 100644
--- a/app/assets/javascripts/profile/account/index.js
+++ b/app/assets/javascripts/profile/account/index.js
@@ -1,7 +1,8 @@
import Vue from 'vue';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Translate from '~/vue_shared/translate';
-import UpdateUsername from './components/update_username.vue';
import deleteAccountModal from './components/delete_account_modal.vue';
+import UpdateUsername from './components/update_username.vue';
export default () => {
Vue.use(Translate);
@@ -31,7 +32,7 @@ export default () => {
mounted() {
deleteAccountButton.classList.remove('disabled');
deleteAccountButton.addEventListener('click', () => {
- this.$root.$emit('bv::show::modal', 'delete-account-modal', '#delete-account-button');
+ this.$root.$emit(BV_SHOW_MODAL, 'delete-account-modal', '#delete-account-button');
});
},
render(createElement) {
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index 8b2006b7c5b..184ee3810ac 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -1,31 +1,92 @@
<script>
-import { s__ } from '~/locale';
+import { GlButton } from '@gitlab/ui';
+import createFlash, { FLASH_TYPES } from '~/flash';
+import { INTEGRATION_VIEW_CONFIGS, i18n } from '../constants';
import IntegrationView from './integration_view.vue';
-const INTEGRATION_VIEW_CONFIGS = {
- sourcegraph: {
- title: s__('ProfilePreferences|Sourcegraph'),
- label: s__('ProfilePreferences|Enable integrated code intelligence on code views'),
- formName: 'sourcegraph_enabled',
- },
- gitpod: {
- title: s__('ProfilePreferences|Gitpod'),
- label: s__('ProfilePreferences|Enable Gitpod integration'),
- formName: 'gitpod_enabled',
- },
-};
+function updateClasses(bodyClasses = '', applicationTheme, layout) {
+ // Remove body class for any previous theme, re-add current one
+ document.body.classList.remove(...bodyClasses.split(' '));
+ document.body.classList.add(applicationTheme);
+
+ // Toggle container-fluid class
+ if (layout === 'fluid') {
+ document
+ .querySelector('.content-wrapper .container-fluid')
+ .classList.remove('container-limited');
+ } else {
+ document.querySelector('.content-wrapper .container-fluid').classList.add('container-limited');
+ }
+}
export default {
name: 'ProfilePreferences',
components: {
IntegrationView,
+ GlButton,
},
inject: {
integrationViews: {
default: [],
},
+ themes: {
+ default: [],
+ },
+ userFields: {
+ default: {},
+ },
+ formEl: 'formEl',
+ profilePreferencesPath: 'profilePreferencesPath',
+ bodyClasses: 'bodyClasses',
},
integrationViewConfigs: INTEGRATION_VIEW_CONFIGS,
+ i18n,
+ data() {
+ return {
+ isSubmitEnabled: true,
+ };
+ },
+ computed: {
+ applicationThemes() {
+ return this.themes.reduce((themes, theme) => {
+ const { id, ...rest } = theme;
+ return { ...themes, [id]: rest };
+ }, {});
+ },
+ },
+ created() {
+ this.formEl.addEventListener('ajax:beforeSend', this.handleLoading);
+ this.formEl.addEventListener('ajax:success', this.handleSuccess);
+ this.formEl.addEventListener('ajax:error', this.handleError);
+ },
+ beforeDestroy() {
+ this.formEl.removeEventListener('ajax:beforeSend', this.handleLoading);
+ this.formEl.removeEventListener('ajax:success', this.handleSuccess);
+ this.formEl.removeEventListener('ajax:error', this.handleError);
+ },
+ methods: {
+ handleLoading() {
+ this.isSubmitEnabled = false;
+ },
+ handleSuccess(customEvent) {
+ const formData = new FormData(this.formEl);
+ updateClasses(
+ this.bodyClasses,
+ this.applicationThemes[formData.get('user[theme_id]')].css_class,
+ this.selectedLayout,
+ );
+ const { message = this.$options.i18n.defaultSuccess, type = FLASH_TYPES.NOTICE } =
+ customEvent?.detail?.[0] || {};
+ createFlash({ message, type });
+ this.isSubmitEnabled = true;
+ },
+ handleError(customEvent) {
+ const { message = this.$options.i18n.defaultError, type = FLASH_TYPES.ALERT } =
+ customEvent?.detail?.[0] || {};
+ createFlash({ message, type });
+ this.isSubmitEnabled = true;
+ },
+ },
};
</script>
@@ -36,10 +97,10 @@ export default {
</div>
<div v-if="integrationViews.length" class="col-lg-4 profile-settings-sidebar">
<h4 class="gl-mt-0" data-testid="profile-preferences-integrations-heading">
- {{ s__('ProfilePreferences|Integrations') }}
+ {{ $options.i18n.integrations }}
</h4>
<p>
- {{ s__('ProfilePreferences|Customize integrations with third party services.') }}
+ {{ $options.i18n.integrationsDescription }}
</p>
</div>
<div v-if="integrationViews.length" class="col-lg-8">
@@ -52,5 +113,19 @@ export default {
:config="$options.integrationViewConfigs[view.name]"
/>
</div>
+ <div class="col-lg-4 profile-settings-sidebar"></div>
+ <div class="col-lg-8">
+ <div class="form-group">
+ <gl-button
+ variant="success"
+ name="commit"
+ type="submit"
+ :disabled="!isSubmitEnabled"
+ :value="$options.i18n.saveChanges"
+ >
+ {{ $options.i18n.saveChanges }}
+ </gl-button>
+ </div>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/profile/preferences/constants.js b/app/assets/javascripts/profile/preferences/constants.js
new file mode 100644
index 00000000000..ea8464ba065
--- /dev/null
+++ b/app/assets/javascripts/profile/preferences/constants.js
@@ -0,0 +1,22 @@
+import { s__, __ } from '~/locale';
+
+export const INTEGRATION_VIEW_CONFIGS = {
+ sourcegraph: {
+ title: s__('Preferences|Sourcegraph'),
+ label: s__('Preferences|Enable integrated code intelligence on code views'),
+ formName: 'sourcegraph_enabled',
+ },
+ gitpod: {
+ title: s__('Preferences|Gitpod'),
+ label: s__('Preferences|Enable Gitpod integration'),
+ formName: 'gitpod_enabled',
+ },
+};
+
+export const i18n = {
+ saveChanges: __('Save changes'),
+ defaultSuccess: __('Preferences saved.'),
+ defaultError: s__('Preferences|Failed to save preferences.'),
+ integrations: s__('Preferences|Integrations'),
+ integrationsDescription: s__('Preferences|Customize integrations with third party services.'),
+};
diff --git a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
index 744e0174a4e..6520e68d41c 100644
--- a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
+++ b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
@@ -3,16 +3,20 @@ import ProfilePreferences from './components/profile_preferences.vue';
export default () => {
const el = document.querySelector('#js-profile-preferences-app');
- const shouldParse = ['integrationViews', 'userFields'];
+ const formEl = document.querySelector('#profile-preferences-form');
+ const shouldParse = ['integrationViews', 'themes', 'userFields'];
- const provide = Object.keys(el.dataset).reduce((memo, key) => {
- let value = el.dataset[key];
- if (shouldParse.includes(key)) {
- value = JSON.parse(value);
- }
+ const provide = Object.keys(el.dataset).reduce(
+ (memo, key) => {
+ let value = el.dataset[key];
+ if (shouldParse.includes(key)) {
+ value = JSON.parse(value);
+ }
- return { ...memo, [key]: value };
- }, {});
+ return { ...memo, [key]: value };
+ },
+ { formEl },
+ );
return new Vue({
el,
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index bfeeff47163..a7332b81b9f 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
-import { Rails } from '~/lib/utils/rails_ujs';
-import { deprecatedCreateFlash as flash } from '../flash';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { Rails } from '~/lib/utils/rails_ujs';
import TimezoneDropdown, {
formatTimezone,
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
+import { deprecatedCreateFlash as flash } from '../flash';
export default class Profile {
constructor({ form } = {}) {
@@ -42,6 +42,7 @@ export default class Profile {
$('#user_notification_email').on('select2-selecting', (event) => {
setTimeout(this.submitForm.bind(event.currentTarget));
});
+ $('#user_email_opted_in').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
}
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index ddb8956b664..f44661cb139 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,12 +1,12 @@
/* eslint-disable func-names, consistent-return, no-return-assign */
-import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import $ from 'jquery';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
-import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { spriteIcon } from '~/lib/utils/common_utils';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index 4fefc2ed569..e6dd4145cb8 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
-import { __ } from './locale';
-import axios from './lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from './flash';
import { fixTitle } from '~/tooltips';
+import { deprecatedCreateFlash as flash } from './flash';
+import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
const tooltipTitles = {
group: {
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index e68430d7dfd..e1e04d63576 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -2,9 +2,9 @@
import $ from 'jquery';
import Api from './api';
-import ProjectSelectComboButton from './project_select_combo_button';
-import { s__ } from './locale';
import { loadCSSFile } from './lib/utils/css_utils';
+import { s__ } from './locale';
+import ProjectSelectComboButton from './project_select_combo_button';
const projectSelect = () => {
loadCSSFile(gon.select2_css_path)
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
index 3ecc3f1d1d3..3527bcb04c6 100644
--- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
@@ -68,6 +68,7 @@ export default {
autocomplete="off"
:debounce="250"
:placeholder="$options.i18n.searchPlaceholder"
+ data-testid="dropdown-search-box"
@input="searchTermChanged"
/>
<gl-dropdown-item
@@ -77,6 +78,7 @@ export default {
:name="branch"
:is-checked="isSelected(branch)"
is-check-item
+ data-testid="dropdown-item"
@click="selectBranch(branch)"
>
{{ branch }}
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index 6411b1ca921..ed216a91ca0 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -1,8 +1,9 @@
<script>
import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import eventHub from '../event_hub';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import csrf from '~/lib/utils/csrf';
+import eventHub from '../event_hub';
import BranchesDropdown from './branches_dropdown.vue';
export default {
@@ -67,7 +68,7 @@ export default {
methods: {
...mapActions(['clearModal', 'setBranch', 'setSelectedBranch']),
show() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
handlePrimary() {
this.$refs.form.$el.submit();
diff --git a/app/assets/javascripts/projects/commit/components/form_trigger.vue b/app/assets/javascripts/projects/commit/components/form_trigger.vue
index e92854c1ac3..3561b5c2473 100644
--- a/app/assets/javascripts/projects/commit/components/form_trigger.vue
+++ b/app/assets/javascripts/projects/commit/components/form_trigger.vue
@@ -10,6 +10,9 @@ export default {
displayText: {
default: '',
},
+ testId: {
+ default: '',
+ },
},
props: {
openModal: {
@@ -26,7 +29,7 @@ export default {
</script>
<template>
- <gl-link data-is-link="true" data-testid="revert-commit-link" @click="showModal">
+ <gl-link data-is-link="true" :data-testid="testId" @click="showModal">
{{ displayText }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/projects/commit/constants.js b/app/assets/javascripts/projects/commit/constants.js
index 233f43d56b9..b47c744e5fb 100644
--- a/app/assets/javascripts/projects/commit/constants.js
+++ b/app/assets/javascripts/projects/commit/constants.js
@@ -2,6 +2,10 @@ import { s__, __ } from '~/locale';
export const OPEN_REVERT_MODAL = 'openRevertModal';
export const REVERT_MODAL_ID = 'revert-commit-modal';
+export const REVERT_LINK_TEST_ID = 'revert-commit-link';
+export const OPEN_CHERRY_PICK_MODAL = 'openCherryPickModal';
+export const CHERRY_PICK_MODAL_ID = 'cherry-pick-commit-modal';
+export const CHERRY_PICK_LINK_TEST_ID = 'cherry-pick-commit-link';
export const I18N_MODAL = {
startMergeRequest: s__('ChangeTypeAction|Start a %{newMergeRequest} with these changes'),
@@ -20,6 +24,11 @@ export const I18N_REVERT_MODAL = {
actionPrimaryText: s__('ChangeTypeAction|Revert'),
};
+export const I18N_CHERRY_PICK_MODAL = {
+ branchLabel: s__('ChangeTypeAction|Pick into branch'),
+ actionPrimaryText: s__('ChangeTypeAction|Cherry-pick'),
+};
+
export const PREPENDED_MODAL_TEXT = s__(
'ChangeTypeAction|This will create a new commit in order to revert the existing changes.',
);
diff --git a/app/assets/javascripts/projects/commit/index.js b/app/assets/javascripts/projects/commit/index.js
new file mode 100644
index 00000000000..b5fdfc25236
--- /dev/null
+++ b/app/assets/javascripts/projects/commit/index.js
@@ -0,0 +1,11 @@
+import initCherryPickCommitModal from './init_cherry_pick_commit_modal';
+import initCherryPickCommitTrigger from './init_cherry_pick_commit_trigger';
+import initRevertCommitModal from './init_revert_commit_modal';
+import initRevertCommitTrigger from './init_revert_commit_trigger';
+
+export default () => {
+ initRevertCommitModal();
+ initRevertCommitTrigger();
+ initCherryPickCommitModal();
+ initCherryPickCommitTrigger();
+};
diff --git a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
new file mode 100644
index 00000000000..24baa27ff70
--- /dev/null
+++ b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import CommitFormModal from './components/form_modal.vue';
+import {
+ I18N_MODAL,
+ I18N_CHERRY_PICK_MODAL,
+ OPEN_CHERRY_PICK_MODAL,
+ CHERRY_PICK_MODAL_ID,
+} from './constants';
+import createStore from './store';
+
+export default function initInviteMembersModal() {
+ const el = document.querySelector('.js-cherry-pick-commit-modal');
+ if (!el) {
+ return false;
+ }
+
+ const {
+ title,
+ endpoint,
+ branch,
+ pushCode,
+ branchCollaboration,
+ existingBranch,
+ branchesEndpoint,
+ } = el.dataset;
+
+ const store = createStore({
+ endpoint,
+ branchesEndpoint,
+ branch,
+ pushCode: parseBoolean(pushCode),
+ branchCollaboration: parseBoolean(branchCollaboration),
+ defaultBranch: branch,
+ modalTitle: title,
+ existingBranch,
+ });
+
+ return new Vue({
+ el,
+ store,
+ render: (createElement) =>
+ createElement(CommitFormModal, {
+ props: {
+ i18n: { ...I18N_CHERRY_PICK_MODAL, ...I18N_MODAL },
+ openModal: OPEN_CHERRY_PICK_MODAL,
+ modalId: CHERRY_PICK_MODAL_ID,
+ },
+ }),
+ });
+}
diff --git a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js
new file mode 100644
index 00000000000..942451dc96a
--- /dev/null
+++ b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import CommitFormTrigger from './components/form_trigger.vue';
+import { OPEN_CHERRY_PICK_MODAL, CHERRY_PICK_LINK_TEST_ID } from './constants';
+
+export default function initInviteMembersTrigger() {
+ const el = document.querySelector('.js-cherry-pick-commit-trigger');
+
+ if (!el) {
+ return false;
+ }
+
+ const { displayText } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: { displayText, testId: CHERRY_PICK_LINK_TEST_ID },
+ render: (createElement) =>
+ createElement(CommitFormTrigger, { props: { openModal: OPEN_CHERRY_PICK_MODAL } }),
+ });
+}
diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
index ec0600cd25a..df26aa3c830 100644
--- a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
+++ b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
-import CommitFormModal from './components/form_modal.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
-import createStore from './store';
+import CommitFormModal from './components/form_modal.vue';
import {
I18N_MODAL,
I18N_REVERT_MODAL,
@@ -9,6 +8,7 @@ import {
OPEN_REVERT_MODAL,
REVERT_MODAL_ID,
} from './constants';
+import createStore from './store';
export default function initInviteMembersModal() {
const el = document.querySelector('.js-revert-commit-modal');
diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js b/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js
index 0bb57f22663..dc5168524ca 100644
--- a/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js
+++ b/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import RevertCommitTrigger from './components/form_trigger.vue';
-import { OPEN_REVERT_MODAL } from './constants';
+import CommitFormTrigger from './components/form_trigger.vue';
+import { OPEN_REVERT_MODAL, REVERT_LINK_TEST_ID } from './constants';
export default function initInviteMembersTrigger() {
const el = document.querySelector('.js-revert-commit-trigger');
@@ -13,8 +13,8 @@ export default function initInviteMembersTrigger() {
return new Vue({
el,
- provide: { displayText },
+ provide: { displayText, testId: REVERT_LINK_TEST_ID },
render: (createElement) =>
- createElement(RevertCommitTrigger, { props: { openModal: OPEN_REVERT_MODAL } }),
+ createElement(CommitFormTrigger, { props: { openModal: OPEN_REVERT_MODAL } }),
});
}
diff --git a/app/assets/javascripts/projects/commit/store/actions.js b/app/assets/javascripts/projects/commit/store/actions.js
index 2ae0370d579..10135e55351 100644
--- a/app/assets/javascripts/projects/commit/store/actions.js
+++ b/app/assets/javascripts/projects/commit/store/actions.js
@@ -1,7 +1,7 @@
-import * as types from './mutation_types';
-import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { PROJECT_BRANCHES_ERROR } from '../constants';
+import * as types from './mutation_types';
export const clearModal = ({ commit }) => {
commit(types.CLEAR_MODAL);
diff --git a/app/assets/javascripts/projects/commit_box/info/index.js b/app/assets/javascripts/projects/commit_box/info/index.js
index 254d178f013..4bbdb5c2357 100644
--- a/app/assets/javascripts/projects/commit_box/info/index.js
+++ b/app/assets/javascripts/projects/commit_box/info/index.js
@@ -1,7 +1,7 @@
-import { loadBranches } from './load_branches';
-import { initDetailsButton } from './init_details_button';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
+import { initDetailsButton } from './init_details_button';
+import { loadBranches } from './load_branches';
export const initCommitBoxInfo = (containerSelector = '.js-commit-box-info') => {
const containerEl = document.querySelector(containerSelector);
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index 752bb594794..1566232751d 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -1,6 +1,4 @@
<script>
-import { debounce } from 'lodash';
-import { mapState, mapActions } from 'vuex';
import {
GlDropdown,
GlDropdownSectionHeader,
@@ -9,8 +7,10 @@ import {
GlDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { debounce } from 'lodash';
+import { mapState, mapActions } from 'vuex';
import { urlParamsToObject } from '~/lib/utils/common_utils';
+import { redirectTo } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
const tooltipMessage = __('Searching by both author and message is currently not supported.');
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index 359d81f32f7..72d4f0c31e5 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -1,9 +1,9 @@
-import * as Sentry from '~/sentry/wrapper';
-import * as types from './mutation_types';
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+import * as Sentry from '~/sentry/wrapper';
+import * as types from './mutation_types';
export default {
setInitialData({ commit }, data) {
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue
new file mode 100644
index 00000000000..05bd0f1370b
--- /dev/null
+++ b/app/assets/javascripts/projects/compare/components/app.vue
@@ -0,0 +1,89 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import RevisionDropdown from './revision_dropdown.vue';
+
+export default {
+ csrf,
+ components: {
+ RevisionDropdown,
+ GlButton,
+ },
+ props: {
+ projectCompareIndexPath: {
+ type: String,
+ required: true,
+ },
+ refsProjectPath: {
+ type: String,
+ required: true,
+ },
+ paramsFrom: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ paramsTo: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ projectMergeRequestPath: {
+ type: String,
+ required: true,
+ },
+ createMrPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <form
+ ref="form"
+ class="form-inline js-requires-input js-signature-container"
+ method="POST"
+ :action="projectCompareIndexPath"
+ >
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <revision-dropdown
+ :refs-project-path="refsProjectPath"
+ revision-text="Source"
+ params-name="to"
+ :params-branch="paramsTo"
+ />
+ <div class="compare-ellipsis gl-display-inline" data-testid="ellipsis">...</div>
+ <revision-dropdown
+ :refs-project-path="refsProjectPath"
+ revision-text="Target"
+ params-name="from"
+ :params-branch="paramsFrom"
+ />
+ <gl-button category="primary" variant="success" class="gl-ml-3" @click="onSubmit">
+ {{ s__('CompareRevisions|Compare') }}
+ </gl-button>
+ <a
+ v-if="projectMergeRequestPath"
+ :href="projectMergeRequestPath"
+ data-testid="projectMrButton"
+ class="btn btn-default gl-button gl-ml-3"
+ >
+ {{ s__('CompareRevisions|View open merge request') }}
+ </a>
+ <a
+ v-else-if="createMrPath"
+ :href="createMrPath"
+ data-testid="createMrButton"
+ class="btn btn-default gl-button gl-ml-3"
+ >
+ {{ s__('CompareRevisions|Create merge request') }}
+ </a>
+ </form>
+</template>
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
new file mode 100644
index 00000000000..13d80b5ae0b
--- /dev/null
+++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
@@ -0,0 +1,145 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ },
+ props: {
+ refsProjectPath: {
+ type: String,
+ required: true,
+ },
+ revisionText: {
+ type: String,
+ required: true,
+ },
+ paramsName: {
+ type: String,
+ required: true,
+ },
+ paramsBranch: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ branches: [],
+ tags: [],
+ loading: true,
+ searchTerm: '',
+ selectedRevision: this.getDefaultBranch(),
+ };
+ },
+ computed: {
+ filteredBranches() {
+ return this.branches.filter((branch) =>
+ branch.toLowerCase().includes(this.searchTerm.toLowerCase()),
+ );
+ },
+ hasFilteredBranches() {
+ return this.filteredBranches.length;
+ },
+ filteredTags() {
+ return this.tags.filter((tag) => tag.toLowerCase().includes(this.searchTerm.toLowerCase()));
+ },
+ hasFilteredTags() {
+ return this.filteredTags.length;
+ },
+ },
+ mounted() {
+ this.fetchBranchesAndTags();
+ },
+ methods: {
+ fetchBranchesAndTags() {
+ const endpoint = this.refsProjectPath;
+
+ return axios
+ .get(endpoint)
+ .then(({ data }) => {
+ this.branches = data.Branches || [];
+ this.tags = data.Tags || [];
+ })
+ .catch(() => {
+ createFlash({
+ message: `${s__(
+ 'CompareRevisions|There was an error while updating the branch/tag list. Please try again.',
+ )}`,
+ });
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ getDefaultBranch() {
+ return this.paramsBranch || s__('CompareRevisions|Select branch/tag');
+ },
+ onClick(revision) {
+ this.selectedRevision = revision;
+ },
+ onSearchEnter() {
+ this.selectedRevision = this.searchTerm;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`">
+ <div class="input-group inline-input-group">
+ <span class="input-group-prepend">
+ <div class="input-group-text">
+ {{ revisionText }}
+ </div>
+ </span>
+ <input type="hidden" :name="paramsName" :value="selectedRevision" />
+ <gl-dropdown
+ class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace"
+ toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!"
+ :text="selectedRevision"
+ header-text="Select Git revision"
+ :loading="loading"
+ >
+ <template #header>
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ :placeholder="s__('CompareRevisions|Filter by Git revision')"
+ @keyup.enter="onSearchEnter"
+ />
+ </template>
+ <gl-dropdown-section-header v-if="hasFilteredBranches">
+ {{ s__('CompareRevisions|Branches') }}
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="(branch, index) in filteredBranches"
+ :key="`branch${index}`"
+ is-check-item
+ :is-checked="selectedRevision === branch"
+ @click="onClick(branch)"
+ >
+ {{ branch }}
+ </gl-dropdown-item>
+ <gl-dropdown-section-header v-if="hasFilteredTags">
+ {{ s__('CompareRevisions|Tags') }}
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="(tag, index) in filteredTags"
+ :key="`tag${index}`"
+ is-check-item
+ :is-checked="selectedRevision === tag"
+ @click="onClick(tag)"
+ >
+ {{ tag }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js
new file mode 100644
index 00000000000..4337eecb667
--- /dev/null
+++ b/app/assets/javascripts/projects/compare/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import CompareApp from './components/app.vue';
+
+export default function init() {
+ const el = document.getElementById('js-compare-selector');
+ const {
+ refsProjectPath,
+ paramsFrom,
+ paramsTo,
+ projectCompareIndexPath,
+ projectMergeRequestPath,
+ createMrPath,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ components: {
+ CompareApp,
+ },
+ render(createElement) {
+ return createElement(CompareApp, {
+ props: {
+ refsProjectPath,
+ paramsFrom,
+ paramsTo,
+ projectCompareIndexPath,
+ projectMergeRequestPath,
+ createMrPath,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue
index 5429d51dae0..81d23a563e2 100644
--- a/app/assets/javascripts/projects/components/project_delete_button.vue
+++ b/app/assets/javascripts/projects/components/project_delete_button.vue
@@ -22,10 +22,10 @@ export default {
strings: {
alertTitle: __('You are about to permanently delete this project'),
alertBody: __(
- 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.',
+ 'Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.',
),
modalBody: __(
- "This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc.",
+ "This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc.",
),
},
};
diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue
index 051bfcb732a..2e46f437ace 100644
--- a/app/assets/javascripts/projects/components/shared/delete_button.vue
+++ b/app/assets/javascripts/projects/components/shared/delete_button.vue
@@ -1,8 +1,8 @@
<script>
-import { uniqueId } from 'lodash';
import { GlModal, GlModalDirective, GlFormInput, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { uniqueId } from 'lodash';
import csrf from '~/lib/utils/csrf';
+import { __ } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
index b54f7051806..ef61fba88fe 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
@@ -1,14 +1,14 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import WelcomePage from './welcome.vue';
-import LegacyContainer from './legacy_container.vue';
import { __, s__ } from '~/locale';
import blankProjectIllustration from '../illustrations/blank-project.svg';
+import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
import createFromTemplateIllustration from '../illustrations/create-from-template.svg';
import importProjectIllustration from '../illustrations/import-project.svg';
-import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
+import LegacyContainer from './legacy_container.vue';
+import WelcomePage from './welcome.vue';
const BLANK_PANEL = 'blank_project';
const CI_CD_PANEL = 'cicd_for_external_repo';
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/index.js b/app/assets/javascripts/projects/experiment_new_project_creation/index.js
index 06920a5ab19..0414f7ef6a5 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/index.js
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import NewProjectCreationApp from './components/app.vue';
-export default function (el, props) {
+export default function initNewProjectCreation(el, props) {
return new Vue({
el,
components: {
diff --git a/app/assets/javascripts/projects/members/constants.js b/app/assets/javascripts/projects/members/constants.js
new file mode 100644
index 00000000000..a69a64fe882
--- /dev/null
+++ b/app/assets/javascripts/projects/members/constants.js
@@ -0,0 +1 @@
+export const PROJECT_MEMBER_BASE_PROPERTY_NAME = 'project_member';
diff --git a/app/assets/javascripts/projects/members/utils.js b/app/assets/javascripts/projects/members/utils.js
new file mode 100644
index 00000000000..a8a635e3ce8
--- /dev/null
+++ b/app/assets/javascripts/projects/members/utils.js
@@ -0,0 +1,8 @@
+import { MEMBER_ACCESS_LEVEL_PROPERTY_NAME } from '~/members/constants';
+import { baseRequestFormatter } from '~/members/utils';
+import { PROJECT_MEMBER_BASE_PROPERTY_NAME } from './constants';
+
+export const projectMemberRequestFormatter = baseRequestFormatter(
+ PROJECT_MEMBER_BASE_PROPERTY_NAME,
+ MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
+);
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 7bb62cf4a73..4a8e1424fa8 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -1,44 +1,12 @@
<script>
-import { GlAlert, GlTabs, GlTab } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql';
-import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql';
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import PipelineCharts from './pipeline_charts.vue';
-import {
- DEFAULT,
- LOAD_ANALYTICS_FAILURE,
- LOAD_PIPELINES_FAILURE,
- PARSE_FAILURE,
- UNSUPPORTED_DATA,
-} from '../constants';
-
-const defaultAnalyticsValues = {
- weekPipelinesTotals: [],
- weekPipelinesLabels: [],
- weekPipelinesSuccessful: [],
- monthPipelinesLabels: [],
- monthPipelinesTotals: [],
- monthPipelinesSuccessful: [],
- yearPipelinesLabels: [],
- yearPipelinesTotals: [],
- yearPipelinesSuccessful: [],
- pipelineTimesLabels: [],
- pipelineTimesValues: [],
-};
-
-const defaultCountValues = {
- totalPipelines: {
- count: 0,
- },
- successfulPipelines: {
- count: 0,
- },
-};
+const charts = ['pipelines', 'deployments'];
export default {
components: {
- GlAlert,
GlTabs,
GlTab,
PipelineCharts,
@@ -50,171 +18,42 @@ export default {
type: Boolean,
default: false,
},
- projectPath: {
- type: String,
- default: '',
- },
},
data() {
return {
- showFailureAlert: false,
- failureType: null,
- analytics: { ...defaultAnalyticsValues },
- counts: { ...defaultCountValues },
+ selectedTab: 0,
};
},
- apollo: {
- counts: {
- query: getPipelineCountByStatus,
- variables() {
- return {
- projectPath: this.projectPath,
- };
- },
- update(data) {
- return data?.project;
- },
- error() {
- this.reportFailure(LOAD_PIPELINES_FAILURE);
- },
- },
- analytics: {
- query: getProjectPipelineStatistics,
- variables() {
- return {
- projectPath: this.projectPath,
- };
- },
- update(data) {
- return data?.project?.pipelineAnalytics;
- },
- error() {
- this.reportFailure(LOAD_ANALYTICS_FAILURE);
- },
- },
- },
- computed: {
- failure() {
- switch (this.failureType) {
- case LOAD_ANALYTICS_FAILURE:
- return {
- text: this.$options.errorTexts[LOAD_ANALYTICS_FAILURE],
- variant: 'danger',
- };
- case PARSE_FAILURE:
- return {
- text: this.$options.errorTexts[PARSE_FAILURE],
- variant: 'danger',
- };
- case UNSUPPORTED_DATA:
- return {
- text: this.$options.errorTexts[UNSUPPORTED_DATA],
- variant: 'info',
- };
- default:
- return {
- text: this.$options.errorTexts[DEFAULT],
- variant: 'danger',
- };
- }
- },
- lastWeekChartData() {
- return {
- labels: this.analytics.weekPipelinesLabels,
- totals: this.analytics.weekPipelinesTotals,
- success: this.analytics.weekPipelinesSuccessful,
- };
- },
- lastMonthChartData() {
- return {
- labels: this.analytics.monthPipelinesLabels,
- totals: this.analytics.monthPipelinesTotals,
- success: this.analytics.monthPipelinesSuccessful,
- };
- },
- lastYearChartData() {
- return {
- labels: this.analytics.yearPipelinesLabels,
- totals: this.analytics.yearPipelinesTotals,
- success: this.analytics.yearPipelinesSuccessful,
- };
- },
- timesChartData() {
- return {
- labels: this.analytics.pipelineTimesLabels,
- values: this.analytics.pipelineTimesValues,
- };
- },
- successRatio() {
- const { successfulPipelines, failedPipelines } = this.counts;
- const successfulCount = successfulPipelines?.count;
- const failedCount = failedPipelines?.count;
- const ratio = (successfulCount / (successfulCount + failedCount)) * 100;
-
- return failedCount === 0 ? 100 : ratio;
- },
- formattedCounts() {
- const { totalPipelines, successfulPipelines, failedPipelines } = this.counts;
-
- return {
- total: totalPipelines?.count,
- success: successfulPipelines?.count,
- failed: failedPipelines?.count,
- successRatio: this.successRatio,
- };
- },
+ created() {
+ this.selectTab();
+ window.addEventListener('popstate', this.selectTab);
},
methods: {
- hideAlert() {
- this.showFailureAlert = false;
- },
- reportFailure(type) {
- this.showFailureAlert = true;
- this.failureType = type;
+ selectTab() {
+ const [chart] = getParameterValues('chart') || charts;
+ const tab = charts.indexOf(chart);
+ this.selectedTab = tab >= 0 ? tab : 0;
+ },
+ onTabChange(index) {
+ if (index !== this.selectedTab) {
+ this.selectedTab = index;
+ const path = mergeUrlParams({ chart: charts[index] }, window.location.pathname);
+ updateHistory({ url: path, title: window.title });
+ }
},
},
- errorTexts: {
- [LOAD_ANALYTICS_FAILURE]: s__(
- 'PipelineCharts|An error has ocurred when retrieving the analytics data',
- ),
- [LOAD_PIPELINES_FAILURE]: s__(
- 'PipelineCharts|An error has ocurred when retrieving the pipelines data',
- ),
- [PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'),
- [DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'),
- },
};
</script>
<template>
<div>
- <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{
- failure.text
- }}</gl-alert>
- <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts">
+ <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts" :value="selectedTab" @input="onTabChange">
<gl-tab :title="__('Pipelines')">
- <pipeline-charts
- :counts="formattedCounts"
- :last-week="lastWeekChartData"
- :last-month="lastMonthChartData"
- :last-year="lastYearChartData"
- :times-chart="timesChartData"
- :loading="$apollo.queries.counts.loading"
- @report-failure="reportFailure"
- />
+ <pipeline-charts />
</gl-tab>
<gl-tab :title="__('Deployments')">
<deployment-frequency-charts />
</gl-tab>
</gl-tabs>
- <pipeline-charts
- v-else
- :counts="formattedCounts"
- :last-week="lastWeekChartData"
- :last-month="lastMonthChartData"
- :last-year="lastYearChartData"
- :times-chart="timesChartData"
- :loading="$apollo.queries.counts.loading"
- @report-failure="reportFailure"
- />
+ <pipeline-charts v-else />
</div>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue
new file mode 100644
index 00000000000..43b36da8b2c
--- /dev/null
+++ b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlSegmentedControl } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
+
+export default {
+ components: {
+ GlSegmentedControl,
+ CiCdAnalyticsAreaChart,
+ },
+ props: {
+ charts: {
+ required: true,
+ type: Array,
+ },
+ chartOptions: {
+ required: true,
+ type: Object,
+ },
+ },
+ data() {
+ return {
+ selectedChart: 0,
+ };
+ },
+ computed: {
+ chartRanges() {
+ return this.charts.map(({ title }, index) => ({ text: title, value: index }));
+ },
+ chart() {
+ return this.charts[this.selectedChart];
+ },
+ dateRange() {
+ return sprintf(s__('CiCdAnalytics|Date range: %{range}'), { range: this.chart.range });
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-segmented-control v-model="selectedChart" :options="chartRanges" class="gl-mb-4" />
+ <ci-cd-analytics-area-chart
+ v-if="chart"
+ :chart-data="chart.data"
+ :area-chart-options="chartOptions"
+ >
+ {{ dateRange }}
+ </ci-cd-analytics-area-chart>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
index bec4ab407f0..733f833d51a 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
@@ -1,63 +1,184 @@
<script>
-import dateFormat from 'dateformat';
+import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { GlSkeletonLoader } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
+import dateFormat from 'dateformat';
import { getDateInPast } from '~/lib/utils/datetime_utility';
+import { __, s__, sprintf } from '~/locale';
import {
+ DEFAULT,
CHART_CONTAINER_HEIGHT,
CHART_DATE_FORMAT,
INNER_CHART_HEIGHT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
+ ONE_YEAR_AGO_DAYS,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
PARSE_FAILURE,
+ LOAD_ANALYTICS_FAILURE,
+ LOAD_PIPELINES_FAILURE,
+ UNSUPPORTED_DATA,
} from '../constants';
+import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql';
+import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql';
+import CiCdAnalyticsCharts from './ci_cd_analytics_charts.vue';
import StatisticsList from './statistics_list.vue';
-import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
+
+const defaultAnalyticsValues = {
+ weekPipelinesTotals: [],
+ weekPipelinesLabels: [],
+ weekPipelinesSuccessful: [],
+ monthPipelinesLabels: [],
+ monthPipelinesTotals: [],
+ monthPipelinesSuccessful: [],
+ yearPipelinesLabels: [],
+ yearPipelinesTotals: [],
+ yearPipelinesSuccessful: [],
+ pipelineTimesLabels: [],
+ pipelineTimesValues: [],
+};
+
+const defaultCountValues = {
+ totalPipelines: {
+ count: 0,
+ },
+ successfulPipelines: {
+ count: 0,
+ },
+};
export default {
components: {
+ GlAlert,
GlColumnChart,
GlSkeletonLoader,
StatisticsList,
- CiCdAnalyticsAreaChart,
+ CiCdAnalyticsCharts,
+ },
+ inject: {
+ projectPath: {
+ type: String,
+ default: '',
+ },
},
- props: {
+ data() {
+ return {
+ showFailureAlert: false,
+ failureType: null,
+ analytics: { ...defaultAnalyticsValues },
+ counts: { ...defaultCountValues },
+ };
+ },
+ apollo: {
counts: {
- required: true,
- type: Object,
+ query: getPipelineCountByStatus,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update(data) {
+ return data?.project;
+ },
+ error() {
+ this.reportFailure(LOAD_PIPELINES_FAILURE);
+ },
+ },
+ analytics: {
+ query: getProjectPipelineStatistics,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update(data) {
+ return data?.project?.pipelineAnalytics;
+ },
+ error() {
+ this.reportFailure(LOAD_ANALYTICS_FAILURE);
+ },
+ },
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.counts.loading;
},
- loading: {
- required: false,
- default: false,
- type: Boolean,
+ failure() {
+ switch (this.failureType) {
+ case LOAD_ANALYTICS_FAILURE:
+ return {
+ text: this.$options.errorTexts[LOAD_ANALYTICS_FAILURE],
+ variant: 'danger',
+ };
+ case PARSE_FAILURE:
+ return {
+ text: this.$options.errorTexts[PARSE_FAILURE],
+ variant: 'danger',
+ };
+ case UNSUPPORTED_DATA:
+ return {
+ text: this.$options.errorTexts[UNSUPPORTED_DATA],
+ variant: 'info',
+ };
+ default:
+ return {
+ text: this.$options.errorTexts[DEFAULT],
+ variant: 'danger',
+ };
+ }
},
- lastWeek: {
- required: true,
- type: Object,
+ lastWeekChartData() {
+ return {
+ labels: this.analytics.weekPipelinesLabels,
+ totals: this.analytics.weekPipelinesTotals,
+ success: this.analytics.weekPipelinesSuccessful,
+ };
},
- lastMonth: {
- required: true,
- type: Object,
+ lastMonthChartData() {
+ return {
+ labels: this.analytics.monthPipelinesLabels,
+ totals: this.analytics.monthPipelinesTotals,
+ success: this.analytics.monthPipelinesSuccessful,
+ };
},
- lastYear: {
- required: true,
- type: Object,
+ lastYearChartData() {
+ return {
+ labels: this.analytics.yearPipelinesLabels,
+ totals: this.analytics.yearPipelinesTotals,
+ success: this.analytics.yearPipelinesSuccessful,
+ };
},
- timesChart: {
- required: true,
- type: Object,
+ timesChartData() {
+ return {
+ labels: this.analytics.pipelineTimesLabels,
+ values: this.analytics.pipelineTimesValues,
+ };
+ },
+ successRatio() {
+ const { successfulPipelines, failedPipelines } = this.counts;
+ const successfulCount = successfulPipelines?.count;
+ const failedCount = failedPipelines?.count;
+ const ratio = (successfulCount / (successfulCount + failedCount)) * 100;
+
+ return failedCount === 0 ? 100 : ratio;
+ },
+ formattedCounts() {
+ const { totalPipelines, successfulPipelines, failedPipelines } = this.counts;
+
+ return {
+ total: totalPipelines?.count,
+ success: successfulPipelines?.count,
+ failed: failedPipelines?.count,
+ successRatio: this.successRatio,
+ };
},
- },
- computed: {
areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
+ const { lastWeekRange, lastMonthRange, lastYearRange } = this.$options.chartRanges;
const charts = [
- { title: lastWeek, data: this.lastWeek },
- { title: lastMonth, data: this.lastMonth },
- { title: lastYear, data: this.lastYear },
+ { title: lastWeek, range: lastWeekRange, data: this.lastWeekChartData },
+ { title: lastMonth, range: lastMonthRange, data: this.lastMonthChartData },
+ { title: lastYear, range: lastYearRange, data: this.lastYearChartData },
];
let areaChartsData = [];
@@ -65,7 +186,7 @@ export default {
areaChartsData = charts.map(this.buildAreaChartData);
} catch {
areaChartsData = [];
- this.vm.$emit('report-failure', PARSE_FAILURE);
+ this.reportFailure(PARSE_FAILURE);
}
return areaChartsData;
@@ -74,20 +195,28 @@ export default {
return [
{
name: 'full',
- data: this.mergeLabelsAndValues(this.timesChart.labels, this.timesChart.values),
+ data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
},
];
},
},
methods: {
+ hideAlert() {
+ this.showFailureAlert = false;
+ },
+ reportFailure(type) {
+ this.showFailureAlert = true;
+ this.failureType = type;
+ },
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
- buildAreaChartData({ title, data }) {
+ buildAreaChartData({ title, data, range }) {
const { labels, totals, success } = data;
return {
title,
+ range,
data: [
{
name: 'all',
@@ -118,28 +247,50 @@ export default {
},
yAxis: {
name: s__('Pipeline|Pipelines'),
+ minInterval: 1,
},
},
- get chartTitles() {
+ errorTexts: {
+ [LOAD_ANALYTICS_FAILURE]: s__(
+ 'PipelineCharts|An error has ocurred when retrieving the analytics data',
+ ),
+ [LOAD_PIPELINES_FAILURE]: s__(
+ 'PipelineCharts|An error has ocurred when retrieving the pipelines data',
+ ),
+ [PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'),
+ [DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'),
+ },
+ chartTitles: {
+ lastWeek: __('Last week'),
+ lastMonth: __('Last month'),
+ lastYear: __('Last year'),
+ },
+ get chartRanges() {
const today = dateFormat(new Date(), CHART_DATE_FORMAT);
const pastDate = (timeScale) =>
dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
return {
- lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
+ lastWeekRange: sprintf(__('%{oneWeekAgo} - %{today}'), {
oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
today,
}),
- lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
+ lastMonthRange: sprintf(__('%{oneMonthAgo} - %{today}'), {
oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
today,
}),
- lastYear: __('Pipelines for last year'),
+ lastYearRange: sprintf(__('%{oneYearAgo} - %{today}'), {
+ oneYearAgo: pastDate(ONE_YEAR_AGO_DAYS),
+ today,
+ }),
};
},
};
</script>
<template>
<div>
+ <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{
+ failure.text
+ }}</gl-alert>
<div class="gl-mb-3">
<h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
</div>
@@ -147,7 +298,7 @@ export default {
<div class="row">
<div class="col-md-6">
<gl-skeleton-loader v-if="loading" :lines="5" />
- <statistics-list v-else :counts="counts" />
+ <statistics-list v-else :counts="formattedCounts" />
</div>
<div v-if="!loading" class="col-md-6">
<strong>{{ __('Duration for the last 30 commits') }}</strong>
@@ -164,13 +315,7 @@ export default {
<template v-if="!loading">
<hr />
<h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4>
- <ci-cd-analytics-area-chart
- v-for="(chart, index) in areaCharts"
- :key="index"
- :chart-data="chart.data"
- :area-chart-options="$options.areaChartOptions"
- >{{ chart.title }}</ci-cd-analytics-area-chart
- >
+ <ci-cd-analytics-charts :charts="areaCharts" :chart-options="$options.areaChartOptions" />
</template>
</div>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js
index 079e23943c1..41fe81f21ea 100644
--- a/app/assets/javascripts/projects/pipelines/charts/constants.js
+++ b/app/assets/javascripts/projects/pipelines/charts/constants.js
@@ -10,6 +10,8 @@ export const ONE_WEEK_AGO_DAYS = 7;
export const ONE_MONTH_AGO_DAYS = 31;
+export const ONE_YEAR_AGO_DAYS = 365;
+
export const CHART_DATE_FORMAT = 'dd mmm';
export const DEFAULT = 'default';
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index a62b5d423de..a5e53ee3927 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -1,17 +1,16 @@
/* eslint-disable no-underscore-dangle, class-methods-use-this */
import { escape, find, countBy } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { n__, s__, __, sprintf } from '~/locale';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class AccessDropdown {
constructor(options) {
const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
this.options = options;
this.hasLicense = hasLicense;
- this.deployKeysOnProtectedBranchesEnabled = gon.features.deployKeysOnProtectedBranches;
this.groups = [];
this.accessLevel = accessLevel;
this.accessLevelsData = accessLevelsData.roles;
@@ -330,11 +329,7 @@ export default class AccessDropdown {
);
})
.catch(() => {
- if (this.deployKeysOnProtectedBranchesEnabled) {
- createFlash({ message: __('Failed to load groups, users and deploy keys.') });
- } else {
- createFlash({ message: __('Failed to load groups & users.') });
- }
+ createFlash({ message: __('Failed to load groups, users and deploy keys.') });
});
} else {
this.getDeployKeys(query)
@@ -445,35 +440,33 @@ export default class AccessDropdown {
}
}
- if (this.deployKeysOnProtectedBranchesEnabled) {
- const deployKeys = deployKeysResponse.map((response) => {
- const {
- id,
- fingerprint,
- title,
- owner: { avatar_url, name, username },
- } = response;
-
- const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`;
-
- return {
- id,
- title: title.concat(' ', shortFingerprint),
- avatar_url,
- fullname: name,
- username,
- type: LEVEL_TYPES.DEPLOY_KEY,
- };
- });
+ const deployKeys = deployKeysResponse.map((response) => {
+ const {
+ id,
+ fingerprint,
+ title,
+ owner: { avatar_url, name, username },
+ } = response;
+
+ const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`;
+
+ return {
+ id,
+ title: title.concat(' ', shortFingerprint),
+ avatar_url,
+ fullname: name,
+ username,
+ type: LEVEL_TYPES.DEPLOY_KEY,
+ };
+ });
- if (this.accessLevel === ACCESS_LEVELS.PUSH) {
- if (deployKeys.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
- deployKeys,
- );
- }
+ if (this.accessLevel === ACCESS_LEVELS.PUSH) {
+ if (deployKeys.length) {
+ consolidatedData = consolidatedData.concat(
+ [{ type: 'divider' }],
+ [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
+ deployKeys,
+ );
}
}
@@ -501,19 +494,15 @@ export default class AccessDropdown {
}
getDeployKeys(query) {
- if (this.deployKeysOnProtectedBranchesEnabled) {
- return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), {
- params: {
- search: query,
- per_page: 20,
- active: true,
- project_id: gon.current_project_id,
- push_code: true,
- },
- });
- }
-
- return Promise.resolve({ data: [] });
+ return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), {
+ params: {
+ search: query,
+ per_page: 20,
+ active: true,
+ project_id: gon.current_project_id,
+ push_code: true,
+ },
+ });
}
buildUrl(urlRoot, url) {
diff --git a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
index 51281def7d0..0786a74f6b1 100644
--- a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
+++ b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert, GlToggle, GlTooltip } from '@gitlab/ui';
-import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
const DEFAULT_ERROR_MESSAGE = __('An error occurred while updating the configuration.');
diff --git a/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js b/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
index c5d45fe6fed..eaeb5848b68 100644
--- a/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
+++ b/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import SharedRunnersToggle from '~/projects/settings/components/shared_runners_toggle.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import SharedRunnersToggle from '~/projects/settings/components/shared_runners_toggle.vue';
export default (containerId = 'toggle-shared-runners-form') => {
const containerEl = document.getElementById(containerId);
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index 909f1afd9f6..9b3c0dd2755 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -1,61 +1,43 @@
<script>
import { GlAlert } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import ServiceDeskSetting from './service_desk_setting.vue';
-import ServiceDeskService from '../services/service_desk_service';
-import eventHub from '../event_hub';
export default {
- name: 'ServiceDeskRoot',
components: {
GlAlert,
ServiceDeskSetting,
},
- props: {
+ inject: {
initialIsEnabled: {
- type: Boolean,
- required: true,
+ default: false,
},
endpoint: {
- type: String,
- required: true,
+ default: '',
},
- incomingEmail: {
- type: String,
- required: false,
+ initialIncomingEmail: {
default: '',
},
customEmail: {
- type: String,
- required: false,
default: '',
},
customEmailEnabled: {
- type: Boolean,
- required: false,
+ default: false,
},
selectedTemplate: {
- type: String,
- required: false,
default: '',
},
outgoingName: {
- type: String,
- required: false,
default: '',
},
projectKey: {
- type: String,
- required: false,
default: '',
},
templates: {
- type: Array,
- required: false,
- default: () => [],
+ default: [],
},
},
-
data() {
return {
isEnabled: this.initialIsEnabled,
@@ -63,28 +45,21 @@ export default {
isAlertShowing: false,
alertVariant: 'danger',
alertMessage: '',
+ incomingEmail: this.initialIncomingEmail,
updatedCustomEmail: this.customEmail,
};
},
-
- created() {
- eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
- eventHub.$on('serviceDeskTemplateSave', this.onSaveTemplate);
- this.service = new ServiceDeskService(this.endpoint);
- },
-
- beforeDestroy() {
- eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
- eventHub.$off('serviceDeskTemplateSave', this.onSaveTemplate);
- },
-
methods: {
onEnableToggled(isChecked) {
this.isEnabled = isChecked;
this.incomingEmail = '';
- this.service
- .toggleServiceDesk(isChecked)
+ const body = {
+ service_desk_enabled: isChecked,
+ };
+
+ return axios
+ .put(this.endpoint, body)
.then(({ data }) => {
const email = data.service_desk_address;
if (isChecked && !email) {
@@ -104,8 +79,16 @@ export default {
onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) {
this.isTemplateSaving = true;
- this.service
- .updateTemplate({ selectedTemplate, outgoingName, projectKey }, this.isEnabled)
+
+ const body = {
+ issue_template_key: selectedTemplate,
+ outgoing_name: outgoingName,
+ project_key: projectKey,
+ service_desk_enabled: this.isEnabled,
+ };
+
+ return axios
+ .put(this.endpoint, body)
.then(({ data }) => {
this.updatedCustomEmail = data?.service_desk_address;
this.showAlert(__('Changes saved.'), 'success');
@@ -150,6 +133,8 @@ export default {
:initial-project-key="projectKey"
:templates="templates"
:is-template-saving="isTemplateSaving"
+ @save="onSaveTemplate"
+ @toggle="onEnableToggled"
/>
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index a850374fc88..39d9a6a4239 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -1,12 +1,9 @@
<script>
import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import eventHub from '../event_hub';
export default {
- name: 'ServiceDeskSetting',
components: {
ClipboardButton,
GlButton,
@@ -15,7 +12,6 @@ export default {
GlLoadingIcon,
GlSprintf,
},
- mixins: [glFeatureFlagsMixin()],
props: {
isEnabled: {
type: Boolean,
@@ -84,10 +80,10 @@ export default {
},
methods: {
onCheckboxToggle(isChecked) {
- eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
+ this.$emit('toggle', isChecked);
},
onSaveTemplate() {
- eventHub.$emit('serviceDeskTemplateSave', {
+ this.$emit('save', {
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
@@ -111,7 +107,11 @@ export default {
</label>
<div v-if="isEnabled" class="row mt-3">
<div class="col-md-9 mb-0">
- <strong id="incoming-email-describer" class="d-block mb-1">
+ <strong
+ id="incoming-email-describer"
+ class="gl-display-block gl-mb-1"
+ data-testid="incoming-email-describer"
+ >
{{ __('Email address to use for Support Desk') }}
</strong>
<template v-if="email">
@@ -128,11 +128,7 @@ export default {
disabled="true"
/>
<div class="input-group-append">
- <clipboard-button
- :title="__('Copy')"
- :text="email"
- css-class="input-group-text qa-clipboard-button"
- />
+ <clipboard-button :title="__('Copy')" :text="email" css-class="input-group-text" />
</div>
</div>
<span v-if="hasCustomEmail" class="form-text text-muted">
diff --git a/app/assets/javascripts/projects/settings_service_desk/event_hub.js b/app/assets/javascripts/projects/settings_service_desk/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/projects/settings_service_desk/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index 8f9828dd73d..f842ffaaa2b 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -3,43 +3,37 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import ServiceDeskRoot from './components/service_desk_root.vue';
export default () => {
- const serviceDeskRootElement = document.querySelector('.js-service-desk-setting-root');
- if (serviceDeskRootElement) {
- // eslint-disable-next-line no-new
- new Vue({
- el: serviceDeskRootElement,
- components: {
- ServiceDeskRoot,
- },
- data() {
- const { dataset } = serviceDeskRootElement;
- return {
- initialIsEnabled: parseBoolean(dataset.enabled),
- endpoint: dataset.endpoint,
- incomingEmail: dataset.incomingEmail,
- customEmail: dataset.customEmail,
- customEmailEnabled: parseBoolean(dataset.customEmailEnabled),
- selectedTemplate: dataset.selectedTemplate,
- outgoingName: dataset.outgoingName,
- projectKey: dataset.projectKey,
- templates: JSON.parse(dataset.templates),
- };
- },
- render(createElement) {
- return createElement('service-desk-root', {
- props: {
- initialIsEnabled: this.initialIsEnabled,
- endpoint: this.endpoint,
- incomingEmail: this.incomingEmail,
- customEmail: this.customEmail,
- customEmailEnabled: this.customEmailEnabled,
- selectedTemplate: this.selectedTemplate,
- outgoingName: this.outgoingName,
- projectKey: this.projectKey,
- templates: this.templates,
- },
- });
- },
- });
+ const el = document.querySelector('.js-service-desk-setting-root');
+
+ if (!el) {
+ return false;
}
+
+ const {
+ customEmail,
+ customEmailEnabled,
+ enabled,
+ endpoint,
+ incomingEmail,
+ outgoingName,
+ projectKey,
+ selectedTemplate,
+ templates,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ customEmail,
+ customEmailEnabled: parseBoolean(customEmailEnabled),
+ endpoint,
+ initialIncomingEmail: incomingEmail,
+ initialIsEnabled: parseBoolean(enabled),
+ outgoingName,
+ projectKey,
+ selectedTemplate,
+ templates: JSON.parse(templates),
+ },
+ render: (createElement) => createElement(ServiceDeskRoot),
+ });
};
diff --git a/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js b/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js
deleted file mode 100644
index b68c5bb876f..00000000000
--- a/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-
-class ServiceDeskService {
- constructor(endpoint) {
- this.endpoint = endpoint;
- }
-
- toggleServiceDesk(enable) {
- return axios.put(this.endpoint, { service_desk_enabled: enable });
- }
-
- updateTemplate({ selectedTemplate, outgoingName, projectKey = '' }, isEnabled) {
- const body = {
- issue_template_key: selectedTemplate,
- outgoing_name: outgoingName,
- project_key: projectKey,
- service_desk_enabled: isEnabled,
- };
- return axios.put(this.endpoint, body);
- }
-}
-
-export default ServiceDeskService;
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 0f01167988d..f3d12e0dd00 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
@@ -1,10 +1,10 @@
<script>
-import Visibility from 'visibilityjs';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import ciIcon from '~/vue_shared/components/ci_icon.vue';
-import Poll from '~/lib/utils/poll';
+import Visibility from 'visibilityjs';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import Poll from '~/lib/utils/poll';
import { __, s__, sprintf } from '~/locale';
+import ciIcon from '~/vue_shared/components/ci_icon.vue';
import CommitPipelineService from '../services/commit_pipeline_service';
export default {
diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
index 91de6d93e19..1a07f5495a1 100644
--- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
+++ b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
@@ -1,10 +1,10 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlButton, GlFormGroup, GlFormInput, GlModal, GlModalDirective } from '@gitlab/ui';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
copyToClipboard: __('Copy'),
diff --git a/app/assets/javascripts/prometheus_metrics/custom_metrics.js b/app/assets/javascripts/prometheus_metrics/custom_metrics.js
index e891b8bf3b6..e10b5bf1807 100644
--- a/app/assets/javascripts/prometheus_metrics/custom_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/custom_metrics.js
@@ -1,10 +1,10 @@
import $ from 'jquery';
import { escape, sortBy } from 'lodash';
-import PrometheusMetrics from './prometheus_metrics';
-import PANEL_STATE from './constants';
import axios from '~/lib/utils/axios_utils';
-import { s__ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { s__ } from '~/locale';
+import PANEL_STATE from './constants';
+import PrometheusMetrics from './prometheus_metrics';
export default class CustomMetrics extends PrometheusMetrics {
constructor(wrapperSelector) {
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 57f9cec9682..821de0560cd 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -2,8 +2,8 @@ import $ from 'jquery';
import { escape } from 'lodash';
import { s__, n__, sprintf } from '~/locale';
import axios from '../lib/utils/axios_utils';
-import PANEL_STATE from './constants';
import { backOff } from '../lib/utils/common_utils';
+import PANEL_STATE from './constants';
export default class PrometheusMetrics {
constructor(wrapperSelector) {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index a5c7b18f709..c9474f0516c 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
-import AccessDropdown from '~/projects/settings/access_dropdown';
-import axios from '~/lib/utils/axios_utils';
-import AccessorUtilities from '~/lib/utils/accessor';
-import { deprecatedCreateFlash as Flash } from '~/flash';
import CreateItemDropdown from '~/create_item_dropdown';
-import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import AccessorUtilities from '~/lib/utils/accessor';
+import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
+import AccessDropdown from '~/projects/settings/access_dropdown';
+import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
export default class ProtectedBranchCreate {
constructor(options) {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index f5f27b67c71..986abeecafa 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,9 +1,9 @@
import { find } from 'lodash';
-import AccessDropdown from '~/projects/settings/access_dropdown';
import axios from '~/lib/utils/axios_utils';
-import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
-import { deprecatedCreateFlash as flash } from '../flash';
import { __ } from '~/locale';
+import AccessDropdown from '~/projects/settings/access_dropdown';
+import { deprecatedCreateFlash as flash } from '../flash';
+import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
export default class ProtectedBranchEdit {
constructor(options) {
diff --git a/app/assets/javascripts/protected_tags/constants.js b/app/assets/javascripts/protected_tags/constants.js
new file mode 100644
index 00000000000..3e71ba62877
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/constants.js
@@ -0,0 +1,3 @@
+import { s__ } from '~/locale';
+
+export const FAILED_TO_UPDATE_TAG_MESSAGE = s__('ProjectSettings|Failed to update tag!');
diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
index 202286a5fb4..4094c300a50 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
@@ -1,5 +1,5 @@
-import { __ } from '~/locale';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __ } from '~/locale';
export default class ProtectedTagAccessDropdown {
constructor(options) {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index eb44f0c67fd..e3f427b8408 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
-import CreateItemDropdown from '../create_item_dropdown';
import { __ } from '~/locale';
+import CreateItemDropdown from '../create_item_dropdown';
+import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagCreate {
constructor() {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index 157ac1c7ebd..ae7855d4638 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,7 +1,7 @@
import { deprecatedCreateFlash as flash } from '../flash';
import axios from '../lib/utils/axios_utils';
+import { FAILED_TO_UPDATE_TAG_MESSAGE } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
-import { __ } from '~/locale';
export default class ProtectedTagEdit {
constructor(options) {
@@ -48,11 +48,8 @@ export default class ProtectedTagEdit {
.catch(() => {
this.$allowedToCreateDropdownButton.enable();
- flash(
- __('Failed to update tag!'),
- 'alert',
- document.querySelector('.js-protected-tags-list'),
- );
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ flash(FAILED_TO_UPDATE_TAG_MESSAGE);
});
}
}
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index 20aec3e12be..8f2805b36f6 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import {
GlDropdown,
GlDropdownDivider,
@@ -10,8 +9,9 @@ import {
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import createStore from '../stores';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { SEARCH_DEBOUNCE_MS, DEFAULT_I18N } from '../constants';
+import createStore from '../stores';
import RefResultsSection from './ref_results_section.vue';
export default {
diff --git a/app/assets/javascripts/ref/stores/mutations.js b/app/assets/javascripts/ref/stores/mutations.js
index 75026a40175..4dc73dabfe2 100644
--- a/app/assets/javascripts/ref/stores/mutations.js
+++ b/app/assets/javascripts/ref/stores/mutations.js
@@ -1,7 +1,7 @@
-import * as types from './mutation_types';
-import { X_TOTAL_HEADER } from '../constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import { X_TOTAL_HEADER } from '../constants';
+import * as types from './mutation_types';
export default {
[types.SET_PROJECT_ID](state, projectId) {
diff --git a/app/assets/javascripts/registry/explorer/components/delete_image.vue b/app/assets/javascripts/registry/explorer/components/delete_image.vue
new file mode 100644
index 00000000000..22fe9fc1da6
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/delete_image.vue
@@ -0,0 +1,76 @@
+<script>
+import { produce } from 'immer';
+import { GRAPHQL_PAGE_SIZE } from '../constants/index';
+import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
+
+export default {
+ props: {
+ id: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ useUpdateFn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ updateImageStatus(store, { data: { destroyContainerRepository } }) {
+ const variables = {
+ id: this.id,
+ first: GRAPHQL_PAGE_SIZE,
+ };
+ const sourceData = store.readQuery({
+ query: getContainerRepositoryDetailsQuery,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftState) => {
+ // eslint-disable-next-line no-param-reassign
+ draftState.containerRepository.status =
+ destroyContainerRepository.containerRepository.status;
+ });
+
+ store.writeQuery({
+ query: getContainerRepositoryDetailsQuery,
+ variables,
+ data,
+ });
+ },
+ doDelete() {
+ this.$emit('start');
+ return this.$apollo
+ .mutate({
+ mutation: deleteContainerRepositoryMutation,
+ variables: {
+ id: this.id,
+ },
+ update: this.useUpdateFn ? this.updateImageStatus : undefined,
+ })
+ .then(({ data }) => {
+ if (data?.destroyContainerRepository?.errors[0]) {
+ this.$emit('error', data?.destroyContainerRepository?.errors);
+ return;
+ }
+ this.$emit('success');
+ })
+ .catch((e) => {
+ // note: we are adding an array to follow the same format of the error raised above
+ this.$emit('error', [e]);
+ })
+ .finally(() => {
+ this.$emit('end');
+ });
+ },
+ },
+ render() {
+ if (this.$scopedSlots?.default) {
+ return this.$scopedSlots.default({ doDelete: this.doDelete });
+ }
+ return null;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
index 96f221bf71d..0432cf1123c 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
@@ -1,7 +1,12 @@
<script>
import { GlModal, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
-import { REMOVE_TAG_CONFIRMATION_TEXT, REMOVE_TAGS_CONFIRMATION_TEXT } from '../../constants/index';
+import {
+ REMOVE_TAG_CONFIRMATION_TEXT,
+ REMOVE_TAGS_CONFIRMATION_TEXT,
+ DELETE_IMAGE_CONFIRMATION_TITLE,
+ DELETE_IMAGE_CONFIRMATION_TEXT,
+} from '../../constants';
export default {
components: {
@@ -14,9 +19,17 @@ export default {
required: false,
default: () => [],
},
+ deleteImage: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
computed: {
- modalAction() {
+ modalTitle() {
+ if (this.deleteImage) {
+ return DELETE_IMAGE_CONFIRMATION_TITLE;
+ }
return n__(
'ContainerRegistry|Remove tag',
'ContainerRegistry|Remove tags',
@@ -24,14 +37,19 @@ export default {
);
},
modalDescription() {
+ if (this.deleteImage) {
+ return {
+ message: DELETE_IMAGE_CONFIRMATION_TEXT,
+ };
+ }
if (this.itemsToBeDeleted.length > 1) {
return {
message: REMOVE_TAGS_CONFIRMATION_TEXT,
item: this.itemsToBeDeleted.length,
};
}
- const [first] = this.itemsToBeDeleted;
+ const [first] = this.itemsToBeDeleted;
return {
message: REMOVE_TAG_CONFIRMATION_TEXT,
item: first?.path,
@@ -51,16 +69,17 @@ export default {
ref="deleteModal"
modal-id="delete-tag-modal"
ok-variant="danger"
- @ok="$emit('confirmDelete')"
+ :action-primary="{ text: __('Confirm'), attributes: { variant: 'danger' } }"
+ :action-cancel="{ text: __('Cancel') }"
+ @primary="$emit('confirmDelete')"
@cancel="$emit('cancelDelete')"
>
- <template #modal-title>{{ modalAction }}</template>
- <template #modal-ok>{{ modalAction }}</template>
+ <template #modal-title>{{ modalTitle }}</template>
<p v-if="modalDescription" data-testid="description">
<gl-sprintf :message="modalDescription.message">
- <template #item
- ><b>{{ modalDescription.item }}</b></template
- >
+ <template #item>
+ <b>{{ modalDescription.item }}</b>
+ </template>
</gl-sprintf>
</p>
</gl-modal>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
index ed02aa264ed..a4b4c08bc34 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
@@ -1,8 +1,8 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlButton } from '@gitlab/ui';
import { sprintf, n__ } from '~/locale';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import {
DETAILS_PAGE_TITLE,
@@ -24,7 +24,7 @@ import {
export default {
name: 'DetailsHeader',
- components: { GlSprintf, TitleArea, MetadataItem },
+ components: { GlSprintf, GlButton, TitleArea, MetadataItem },
mixins: [timeagoMixin],
props: {
image: {
@@ -36,6 +36,11 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
computed: {
visibilityIcon() {
@@ -65,6 +70,9 @@ export default {
[UNFINISHED_STATUS]: { text: CLEANUP_UNFINISHED_TEXT, tooltip: CLEANUP_UNFINISHED_TOOLTIP },
}[this.image?.expirationPolicyCleanupStatus];
},
+ deleteButtonDisabled() {
+ return this.disabled || !this.image.canDelete;
+ },
},
i18n: {
DETAILS_PAGE_TITLE,
@@ -75,11 +83,13 @@ export default {
<template>
<title-area :metadata-loading="metadataLoading">
<template #title>
- <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
- <template #imageName>
- {{ image.name }}
- </template>
- </gl-sprintf>
+ <span data-testid="title">
+ <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
+ <template #imageName>
+ {{ image.name }}
+ </template>
+ </gl-sprintf>
+ </span>
</template>
<template #metadata-tags-count>
<metadata-item icon="tag" :text="tagCountText" data-testid="tags-count" />
@@ -103,5 +113,15 @@ export default {
data-testid="updated-and-visibility"
/>
</template>
+ <template #right-actions>
+ <gl-button
+ v-if="!metadataLoading"
+ variant="danger"
+ :disabled="deleteButtonDisabled"
+ @click="$emit('delete')"
+ >
+ {{ __('Delete') }}
+ </gl-button>
+ </template>
</title-area>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue b/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue
new file mode 100644
index 00000000000..a16d95a6b30
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import {
+ NO_TAGS_TITLE,
+ NO_TAGS_MESSAGE,
+ MISSING_OR_DELETED_IMAGE_TITLE,
+ MISSING_OR_DELETED_IMAGE_MESSAGE,
+} from '../../constants/index';
+
+export default {
+ components: {
+ GlEmptyState,
+ },
+ props: {
+ noContainersImage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isEmptyImage: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ title() {
+ return this.isEmptyImage ? MISSING_OR_DELETED_IMAGE_TITLE : NO_TAGS_TITLE;
+ },
+ description() {
+ return this.isEmptyImage ? MISSING_OR_DELETED_IMAGE_MESSAGE : NO_TAGS_MESSAGE;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="title"
+ :svg-path="noContainersImage"
+ :description="description"
+ class="gl-mx-auto gl-my-0"
+ />
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue b/app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue
deleted file mode 100644
index 0c684d124d5..00000000000
--- a/app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<script>
-import { GlEmptyState } from '@gitlab/ui';
-import {
- EMPTY_IMAGE_REPOSITORY_TITLE,
- EMPTY_IMAGE_REPOSITORY_MESSAGE,
-} from '../../constants/index';
-
-export default {
- components: {
- GlEmptyState,
- },
- props: {
- noContainersImage: {
- type: String,
- required: false,
- default: '',
- },
- },
- i18n: {
- EMPTY_IMAGE_REPOSITORY_TITLE,
- EMPTY_IMAGE_REPOSITORY_MESSAGE,
- },
-};
-</script>
-
-<template>
- <gl-empty-state
- :title="$options.i18n.EMPTY_IMAGE_REPOSITORY_TITLE"
- :svg-path="noContainersImage"
- :description="$options.i18n.EMPTY_IMAGE_REPOSITORY_MESSAGE"
- class="gl-mx-auto gl-my-0"
- />
-</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue
new file mode 100644
index 00000000000..fc1504f6c31
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import {
+ IMAGE_STATUS_MESSAGES,
+ IMAGE_STATUS_TITLES,
+ IMAGE_STATUS_ALERT_TYPE,
+ PACKAGE_DELETE_HELP_PAGE_PATH,
+} from '../../constants';
+
+export default {
+ components: {
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ message() {
+ return IMAGE_STATUS_MESSAGES[this.status];
+ },
+ title() {
+ return IMAGE_STATUS_TITLES[this.status];
+ },
+ variant() {
+ return IMAGE_STATUS_ALERT_TYPE[this.status];
+ },
+ },
+ links: {
+ PACKAGE_DELETE_HELP_PAGE_PATH,
+ },
+};
+</script>
+<template>
+ <gl-alert :title="title" :variant="variant">
+ <span data-testid="message">
+ <gl-sprintf :message="message">
+ <template #link="{ content }">
+ <gl-link :href="$options.links.PACKAGE_DELETE_HELP_PAGE_PATH" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
index 1e0736c4a53..bc10246614a 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
-import TagsListRow from './tags_list_row.vue';
import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE } from '../../constants/index';
+import TagsListRow from './tags_list_row.vue';
export default {
name: 'TagsList',
@@ -20,6 +20,11 @@ export default {
default: true,
required: false,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
i18n: {
REMOVE_TAGS_BUTTON_TITLE,
@@ -37,6 +42,9 @@ export default {
showMultiDeleteButton() {
return this.tags.some((tag) => tag.canDelete) && !this.isMobile;
},
+ multiDeleteButtonIsDisabled() {
+ return !this.hasSelectedItems || this.disabled;
+ },
},
methods: {
updateSelectedItems(name) {
@@ -55,7 +63,7 @@ export default {
<gl-button
v-if="showMultiDeleteButton"
- :disabled="!hasSelectedItems"
+ :disabled="multiDeleteButtonIsDisabled"
category="secondary"
variant="danger"
@click="$emit('delete', selectedItems)"
@@ -70,6 +78,7 @@ export default {
:first="index === 0"
:selected="selectedItems[tag.name]"
:is-mobile="isMobile"
+ :disabled="disabled"
@select="updateSelectedItems(tag.name)"
@delete="$emit('delete', { [tag.name]: true })"
/>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
index 2e4a489f2cb..c66f92bdd67 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
@@ -1,13 +1,12 @@
<script>
import { GlFormCheckbox, GlTooltipDirective, GlSprintf, GlIcon } from '@gitlab/ui';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import { formatDate } from '~/lib/utils/datetime_utility';
-import ListItem from '~/vue_shared/components/registry/list_item.vue';
-import DeleteButton from '../delete_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
DIGEST_LABEL,
@@ -20,6 +19,7 @@ import {
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
} from '../../constants/index';
+import DeleteButton from '../delete_button.vue';
export default {
components: {
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
index 264a3c27cde..9ae5b0f9eb1 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
@@ -1,12 +1,10 @@
<script>
import { GlTooltipDirective, GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
-import { n__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
-import DeleteButton from '../delete_button.vue';
-
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
LIST_DELETE_BUTTON_DISABLED,
@@ -16,6 +14,7 @@ import {
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
} from '../../constants/index';
+import DeleteButton from '../delete_button.vue';
export default {
name: 'ImageListRow',
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
index f01e3c9d24a..8d7505dfbae 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
@@ -1,8 +1,8 @@
<script>
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
-import { n__, sprintf } from '~/locale';
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
+import { n__, sprintf } from '~/locale';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import {
CONTAINER_REGISTRY_TITLE,
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js
index b5627352857..3f04538a18b 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/registry/explorer/constants/details.js
@@ -1,3 +1,4 @@
+import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, __ } from '~/locale';
// Translations strings
@@ -32,18 +33,30 @@ export const CONFIGURATION_DETAILS_ROW_TEST = s__(
export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Delete selected');
+
export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item}. Are you sure?`,
);
export const REMOVE_TAGS_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`,
);
-export const EMPTY_IMAGE_REPOSITORY_TITLE = s__('ContainerRegistry|This image has no active tags');
-export const EMPTY_IMAGE_REPOSITORY_MESSAGE = s__(
+export const NO_TAGS_TITLE = s__('ContainerRegistry|This image has no active tags');
+export const NO_TAGS_MESSAGE = s__(
`ContainerRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`,
);
+
+export const MISSING_OR_DELETED_IMAGE_TITLE = s__(
+ 'ContainerRegistry|The image repository could not be found.',
+);
+export const MISSING_OR_DELETED_IMAGE_MESSAGE = s__(
+ 'ContainerRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page.',
+);
+export const MISSING_OR_DELETE_IMAGE_BREADCRUMB = s__(
+ 'ContainerRegistry|Image repository not found',
+);
+
export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
);
@@ -76,6 +89,29 @@ export const CLEANUP_DISABLED_TOOLTIP = s__(
'ContainerRegistry|Cleanup is disabled for this project',
);
+export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Something went wrong while scheduling the image for deletion.',
+);
+
+export const DELETE_IMAGE_CONFIRMATION_TITLE = s__('ContainerRegistry|Delete image repository?');
+export const DELETE_IMAGE_CONFIRMATION_TEXT = s__(
+ 'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone.',
+);
+
+export const SCHEDULED_FOR_DELETION_STATUS_TITLE = s__(
+ 'ContainerRegistry|Image repository will be deleted',
+);
+export const SCHEDULED_FOR_DELETION_STATUS_MESSAGE = s__(
+ 'ContainerRegistry|This image repository will be deleted. %{linkStart}Learn more.%{linkEnd}',
+);
+
+export const FAILED_DELETION_STATUS_TITLE = s__(
+ 'ContainerRegistry|Image repository deletion failed',
+);
+export const FAILED_DELETION_STATUS_MESSAGE = s__(
+ 'ContainerRegistry|This image repository has failed to be deleted',
+);
+
// Parameters
export const DEFAULT_PAGE = 1;
@@ -85,15 +121,39 @@ export const ALERT_SUCCESS_TAG = 'success_tag';
export const ALERT_DANGER_TAG = 'danger_tag';
export const ALERT_SUCCESS_TAGS = 'success_tags';
export const ALERT_DANGER_TAGS = 'danger_tags';
+export const ALERT_DANGER_IMAGE = 'danger_image';
+
+export const DELETE_SCHEDULED = 'DELETE_SCHEDULED';
+export const DELETE_FAILED = 'DELETE_FAILED';
export const ALERT_MESSAGES = {
[ALERT_SUCCESS_TAG]: DELETE_TAG_SUCCESS_MESSAGE,
[ALERT_DANGER_TAG]: DELETE_TAG_ERROR_MESSAGE,
[ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE,
[ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE,
+ [ALERT_DANGER_IMAGE]: DETAILS_DELETE_IMAGE_ERROR_MESSAGE,
};
export const UNFINISHED_STATUS = 'UNFINISHED';
export const UNSCHEDULED_STATUS = 'UNSCHEDULED';
export const SCHEDULED_STATUS = 'SCHEDULED';
export const ONGOING_STATUS = 'ONGOING';
+
+export const IMAGE_STATUS_TITLES = {
+ [DELETE_SCHEDULED]: SCHEDULED_FOR_DELETION_STATUS_TITLE,
+ [DELETE_FAILED]: FAILED_DELETION_STATUS_TITLE,
+};
+
+export const IMAGE_STATUS_MESSAGES = {
+ [DELETE_SCHEDULED]: SCHEDULED_FOR_DELETION_STATUS_MESSAGE,
+ [DELETE_FAILED]: FAILED_DELETION_STATUS_MESSAGE,
+};
+
+export const IMAGE_STATUS_ALERT_TYPE = {
+ [DELETE_SCHEDULED]: 'info',
+ [DELETE_FAILED]: 'warning',
+};
+
+export const PACKAGE_DELETE_HELP_PAGE_PATH = helpPagePath('user/packages/container_registry', {
+ anchor: 'delete-images',
+});
diff --git a/app/assets/javascripts/registry/explorer/constants/list.js b/app/assets/javascripts/registry/explorer/constants/list.js
index 37ced72861e..f59b9d7a9f5 100644
--- a/app/assets/javascripts/registry/explorer/constants/list.js
+++ b/app/assets/javascripts/registry/explorer/constants/list.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
// Translations strings
@@ -35,8 +35,6 @@ export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__(
export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
'ContainerRegistry|%{title} was successfully scheduled for deletion',
);
-export const IMAGE_REPOSITORY_LIST_LABEL = s__('ContainerRegistry|Image Repositories');
-export const SEARCH_PLACEHOLDER_TEXT = s__('ContainerRegistry|Filter by name');
export const EMPTY_RESULT_TITLE = s__('ContainerRegistry|Sorry, your filter produced no results.');
export const EMPTY_RESULT_MESSAGE = s__(
'ContainerRegistry|To widen your search, change or remove the filters above.',
@@ -47,3 +45,9 @@ export const EMPTY_RESULT_MESSAGE = s__(
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED';
export const GRAPHQL_PAGE_SIZE = 10;
+
+export const SORT_FIELDS = [
+ { orderBy: 'UPDATED', label: __('Updated') },
+ { orderBy: 'CREATED', label: __('Created') },
+ { orderBy: 'NAME', label: __('Name') },
+];
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
index 8b6d778c655..01cb7fa1cab 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
@@ -6,9 +6,17 @@ query getContainerRepositoriesDetails(
$after: String
$before: String
$isGroupPage: Boolean!
+ $sort: ContainerRepositorySort
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
nodes {
id
tagsCount
@@ -16,7 +24,14 @@ query getContainerRepositoriesDetails(
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
nodes {
id
tagsCount
diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/registry/explorer/index.js
index a3890ab5c42..f66839a74bf 100644
--- a/app/assets/javascripts/registry/explorer/index.js
+++ b/app/assets/javascripts/registry/explorer/index.js
@@ -1,12 +1,12 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import Translate from '~/vue_shared/translate';
+import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import PerformancePlugin from '~/performance/vue_performance_plugin';
-import RegistryExplorer from './pages/index.vue';
+import Translate from '~/vue_shared/translate';
import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
-import createRouter from './router';
import { apolloProvider } from './graphql/index';
+import RegistryExplorer from './pages/index.vue';
+import createRouter from './router';
Vue.use(Translate);
Vue.use(GlToast);
@@ -29,7 +29,14 @@ export default () => {
return null;
}
- const { endpoint, expirationPolicy, isGroupPage, isAdmin, ...config } = el.dataset;
+ const {
+ endpoint,
+ expirationPolicy,
+ isGroupPage,
+ isAdmin,
+ showUnfinishedTagCleanupCallout,
+ ...config
+ } = el.dataset;
// This is a mini state to help the breadcrumb have the correct name in the details page
const breadCrumbState = Vue.observable({
@@ -57,6 +64,7 @@ export default () => {
expirationPolicy: expirationPolicy ? JSON.parse(expirationPolicy) : undefined,
isGroupPage: parseBoolean(isGroupPage),
isAdmin: parseBoolean(isAdmin),
+ showUnfinishedTagCleanupCallout: parseBoolean(showUnfinishedTagCleanupCallout),
},
/* eslint-disable @gitlab/require-i18n-strings */
dockerBuildCommand: `docker build -t ${config.repositoryUrl} .`,
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index 0894fd6fcfa..0403467468a 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -2,28 +2,32 @@
import { GlKeysetPagination, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import createFlash from '~/flash';
-import Tracking from '~/tracking';
+import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
+import Tracking from '~/tracking';
+import DeleteImage from '../components/delete_image.vue';
import DeleteAlert from '../components/details_page/delete_alert.vue';
-import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue';
import DeleteModal from '../components/details_page/delete_modal.vue';
import DetailsHeader from '../components/details_page/details_header.vue';
+import EmptyState from '../components/details_page/empty_state.vue';
+import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue';
+import StatusAlert from '../components/details_page/status_alert.vue';
import TagsList from '../components/details_page/tags_list.vue';
import TagsLoader from '../components/details_page/tags_loader.vue';
-import EmptyTagsState from '../components/details_page/empty_tags_state.vue';
-
-import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
-import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
import {
ALERT_SUCCESS_TAG,
ALERT_DANGER_TAG,
ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS,
+ ALERT_DANGER_IMAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
+ MISSING_OR_DELETE_IMAGE_BREADCRUMB,
} from '../constants/index';
+import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
export default {
name: 'RegistryDetailsPage',
@@ -35,7 +39,9 @@ export default {
DeleteModal,
TagsList,
TagsLoader,
- EmptyTagsState,
+ EmptyState,
+ StatusAlert,
+ DeleteImage,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
@@ -53,7 +59,7 @@ export default {
},
result({ data }) {
this.tagsPageInfo = data.containerRepository?.tags?.pageInfo;
- this.breadCrumbState.updateName(data.containerRepository?.name);
+ this.updateBreadcrumb();
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
@@ -68,7 +74,8 @@ export default {
isMobile: false,
mutationLoading: false,
deleteAlertType: null,
- dismissPartialCleanupWarning: false,
+ hidePartialCleanupWarning: false,
+ deleteImageAlert: false,
};
},
computed: {
@@ -86,8 +93,9 @@ export default {
},
showPartialCleanupWarning() {
return (
+ this.config.showUnfinishedTagCleanupCallout &&
this.image?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
- !this.dismissPartialCleanupWarning
+ !this.hidePartialCleanupWarning
);
},
tracking() {
@@ -99,14 +107,32 @@ export default {
showPagination() {
return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
},
+ hasNoTags() {
+ return this.tags.length === 0;
+ },
+ pageActionsAreDisabled() {
+ return Boolean(this.image?.status);
+ },
},
methods: {
+ updateBreadcrumb() {
+ const name = this.image?.name || MISSING_OR_DELETE_IMAGE_BREADCRUMB;
+ this.breadCrumbState.updateName(name);
+ },
deleteTags(toBeDeleted) {
+ this.deleteImageAlert = false;
this.itemsToBeDeleted = this.tags.filter((tag) => toBeDeleted[tag.name]);
this.track('click_button');
this.$refs.deleteModal.show();
},
- async handleDelete() {
+ confirmDelete() {
+ if (this.deleteImageAlert) {
+ this.$refs.deleteImage.doDelete();
+ } else {
+ this.handleDeleteTag();
+ }
+ },
+ async handleDeleteTag() {
this.track('confirm_delete');
const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = [];
@@ -168,51 +194,94 @@ export default {
});
}
},
+ dismissPartialCleanupWarning() {
+ this.hidePartialCleanupWarning = true;
+ axios.post(this.config.userCalloutsPath, {
+ feature_name: this.config.userCalloutId,
+ });
+ },
+ deleteImage() {
+ this.deleteImageAlert = true;
+ this.itemsToBeDeleted = [{ path: this.image.path }];
+ this.$refs.deleteModal.show();
+ },
+ deleteImageError() {
+ this.deleteAlertType = ALERT_DANGER_IMAGE;
+ },
+ deleteImageIniit() {
+ this.itemsToBeDeleted = [];
+ this.mutationLoading = true;
+ },
},
};
</script>
<template>
<div v-gl-resize-observer="handleResize" class="gl-my-3">
- <delete-alert
- v-model="deleteAlertType"
- :garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
- :is-admin="config.isAdmin"
- class="gl-my-2"
- />
+ <template v-if="image">
+ <delete-alert
+ v-model="deleteAlertType"
+ :garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
+ :is-admin="config.isAdmin"
+ class="gl-my-2"
+ />
+
+ <partial-cleanup-alert
+ v-if="showPartialCleanupWarning"
+ :run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
+ :cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
+ @dismiss="dismissPartialCleanupWarning"
+ />
- <partial-cleanup-alert
- v-if="showPartialCleanupWarning"
- :run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
- :cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
- @dismiss="dismissPartialCleanupWarning = true"
- />
+ <status-alert v-if="image.status" :status="image.status" />
- <details-header :image="image" :metadata-loading="isLoading" />
+ <details-header
+ :image="image"
+ :metadata-loading="isLoading"
+ :disabled="pageActionsAreDisabled"
+ @delete="deleteImage"
+ />
- <tags-loader v-if="isLoading" />
- <template v-else>
- <empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" />
+ <tags-loader v-if="isLoading" />
<template v-else>
- <tags-list :tags="tags" :is-mobile="isMobile" @delete="deleteTags" />
- <div class="gl-display-flex gl-justify-content-center">
- <gl-keyset-pagination
- v-if="showPagination"
- :has-next-page="tagsPageInfo.hasNextPage"
- :has-previous-page="tagsPageInfo.hasPreviousPage"
- class="gl-mt-3"
- @prev="fetchPreviousPage"
- @next="fetchNextPage"
+ <empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
+ <template v-else>
+ <tags-list
+ :tags="tags"
+ :is-mobile="isMobile"
+ :disabled="pageActionsAreDisabled"
+ @delete="deleteTags"
/>
- </div>
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ :has-next-page="tagsPageInfo.hasNextPage"
+ :has-previous-page="tagsPageInfo.hasPreviousPage"
+ class="gl-mt-3"
+ @prev="fetchPreviousPage"
+ @next="fetchNextPage"
+ />
+ </div>
+ </template>
</template>
- </template>
- <delete-modal
- ref="deleteModal"
- :items-to-be-deleted="itemsToBeDeleted"
- @confirmDelete="handleDelete"
- @cancel="track('cancel_delete')"
- />
+ <delete-image
+ :id="image.id"
+ ref="deleteImage"
+ use-update-fn
+ @start="deleteImageIniit"
+ @error="deleteImageError"
+ @end="mutationLoading = false"
+ />
+
+ <delete-modal
+ ref="deleteModal"
+ :items-to-be-deleted="itemsToBeDeleted"
+ :delete-image="deleteImageAlert"
+ @confirmDelete="confirmDelete"
+ @cancel="track('cancel_delete')"
+ />
+ </template>
+ <empty-state v-else is-empty-image :no-containers-image="config.noContainersImage" />
</div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 336a997d629..8cad9b4ecfc 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -7,17 +7,15 @@ import {
GlLink,
GlAlert,
GlSkeletonLoader,
- GlSearchBoxByClick,
} from '@gitlab/ui';
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
-import Tracking from '~/tracking';
import createFlash from '~/flash';
+import Tracking from '~/tracking';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import DeleteImage from '../components/delete_image.vue';
import RegistryHeader from '../components/list_page/registry_header.vue';
-import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql';
-import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql';
-
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
@@ -25,13 +23,13 @@ import {
CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
- SEARCH_PLACEHOLDER_TEXT,
- IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
+ SORT_FIELDS,
} from '../constants/index';
+import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql';
export default {
name: 'RegistryListPage',
@@ -58,8 +56,9 @@ export default {
GlLink,
GlAlert,
GlSkeletonLoader,
- GlSearchBoxByClick,
RegistryHeader,
+ DeleteImage,
+ RegistrySearch,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -76,11 +75,10 @@ export default {
CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
- SEARCH_PLACEHOLDER_TEXT,
- IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
},
+ searchConfig: SORT_FIELDS,
apollo: {
baseImages: {
query: getContainerRepositoriesQuery,
@@ -122,7 +120,8 @@ export default {
containerRepositoriesCount: 0,
itemToDelete: {},
deleteAlertType: null,
- searchValue: null,
+ filter: [],
+ sorting: { orderBy: 'UPDATED', sort: 'desc' },
name: null,
mutationLoading: false,
fetchAdditionalDetails: false,
@@ -141,6 +140,7 @@ export default {
queryVariables() {
return {
name: this.name,
+ sort: this.sortBy,
fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
isGroupPage: this.config.isGroupPage,
first: GRAPHQL_PAGE_SIZE,
@@ -165,6 +165,10 @@ export default {
? DELETE_IMAGE_SUCCESS_MESSAGE
: DELETE_IMAGE_ERROR_MESSAGE;
},
+ sortBy() {
+ const { orderBy, sort } = this.sorting;
+ return `${orderBy}_${sort}`.toUpperCase();
+ },
},
mounted() {
// If the two graphql calls - which are not batched - resolve togheter we will have a race
@@ -179,30 +183,6 @@ export default {
this.itemToDelete = item;
this.$refs.deleteModal.show();
},
- handleDeleteImage() {
- this.track('confirm_delete');
- this.mutationLoading = true;
- return this.$apollo
- .mutate({
- mutation: deleteContainerRepositoryMutation,
- variables: {
- id: this.itemToDelete.id,
- },
- })
- .then(({ data }) => {
- if (data?.destroyContainerRepository?.errors[0]) {
- this.deleteAlertType = 'danger';
- } else {
- this.deleteAlertType = 'success';
- }
- })
- .catch(() => {
- this.deleteAlertType = 'danger';
- })
- .finally(() => {
- this.mutationLoading = false;
- });
- },
dismissDeleteAlert() {
this.deleteAlertType = null;
this.itemToDelete = {};
@@ -250,6 +230,20 @@ export default {
});
}
},
+ startDelete() {
+ this.track('confirm_delete');
+ this.mutationLoading = true;
+ },
+ updateSorting(value) {
+ this.sorting = {
+ ...this.sorting,
+ ...value,
+ };
+ },
+ doFilter() {
+ const search = this.filter.find((i) => i.type === 'filtered-search-term');
+ this.name = search?.value?.data;
+ },
},
};
</script>
@@ -302,6 +296,16 @@ export default {
</template>
</registry-header>
+ <registry-search
+ :filter="filter"
+ :sorting="sorting"
+ :tokens="[]"
+ :sortable-fields="$options.searchConfig"
+ @sorting:changed="updateSorting"
+ @filter:changed="filter = $event"
+ @filter:submit="doFilter"
+ />
+
<div v-if="isLoading" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
@@ -317,20 +321,6 @@ export default {
</div>
<template v-else>
<template v-if="images.length > 0 || name">
- <div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader">
- <div class="gl-flex-fill-1">
- <h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
- </div>
- <div>
- <gl-search-box-by-click
- v-model="searchValue"
- :placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
- @clear="name = null"
- @submit="name = $event"
- />
- </div>
- </div>
-
<image-list
v-if="images.length"
:images="images"
@@ -358,23 +348,32 @@ export default {
</template>
</template>
- <gl-modal
- ref="deleteModal"
- modal-id="delete-image-modal"
- ok-variant="danger"
- @ok="handleDeleteImage"
- @cancel="track('cancel_delete')"
+ <delete-image
+ :id="itemToDelete.id"
+ @start="startDelete"
+ @error="deleteAlertType = 'danger'"
+ @success="deleteAlertType = 'success'"
+ @end="mutationLoading = false"
>
- <template #modal-title>{{ $options.i18n.REMOVE_REPOSITORY_LABEL }}</template>
- <p>
- <gl-sprintf :message="$options.i18n.REMOVE_REPOSITORY_MODAL_TEXT">
- <template #title>
- <b>{{ itemToDelete.path }}</b>
- </template>
- </gl-sprintf>
- </p>
- <template #modal-ok>{{ __('Remove') }}</template>
- </gl-modal>
+ <template #default="{ doDelete }">
+ <gl-modal
+ ref="deleteModal"
+ modal-id="delete-image-modal"
+ :action-primary="{ text: __('Remove'), attributes: { variant: 'danger' } }"
+ @primary="doDelete"
+ @cancel="track('cancel_delete')"
+ >
+ <template #modal-title>{{ $options.i18n.REMOVE_REPOSITORY_LABEL }}</template>
+ <p>
+ <gl-sprintf :message="$options.i18n.REMOVE_REPOSITORY_MODAL_TEXT">
+ <template #title>
+ <b>{{ itemToDelete.path }}</b>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-modal>
+ </template>
+ </delete-image>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/router.js b/app/assets/javascripts/registry/explorer/router.js
index d8903cf0931..a0c4417d549 100644
--- a/app/assets/javascripts/registry/explorer/router.js
+++ b/app/assets/javascripts/registry/explorer/router.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
-import List from './pages/list.vue';
-import Details from './pages/details.vue';
import { CONTAINER_REGISTRY_TITLE } from './constants/index';
+import Details from './pages/details.vue';
+import List from './pages/list.vue';
Vue.use(VueRouter);
diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
index 66eb681784e..480590ec71e 100644
--- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
+++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
@@ -1,7 +1,6 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { isEqual, get, isEmpty } from 'lodash';
-import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.query.graphql';
import {
FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_TITLE,
@@ -9,6 +8,7 @@ import {
UNAVAILABLE_USER_FEATURE_TEXT,
UNAVAILABLE_ADMIN_FEATURE_TEXT,
} from '../constants';
+import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.query.graphql';
import SettingsForm from './settings_form.vue';
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue
index 7043cea49ba..eb731c382e1 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/registry/settings/components/settings_form.vue
@@ -1,6 +1,5 @@
<script>
import { GlCard, GlButton, GlSprintf } from '@gitlab/ui';
-import Tracking from '~/tracking';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
@@ -19,13 +18,14 @@ import {
CADENCE_LABEL,
EXPIRATION_POLICY_FOOTER_NOTE,
} from '~/registry/settings/constants';
-import { formOptionsGenerator } from '~/registry/settings/utils';
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql';
import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
+import { formOptionsGenerator } from '~/registry/settings/utils';
+import Tracking from '~/tracking';
import ExpirationDropdown from './expiration_dropdown.vue';
import ExpirationInput from './expiration_input.vue';
-import ExpirationToggle from './expiration_toggle.vue';
import ExpirationRunText from './expiration_run_text.vue';
+import ExpirationToggle from './expiration_toggle.vue';
export default {
components: {
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
index 6a4584b1b28..65af6f846aa 100644
--- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -1,7 +1,7 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import Translate from '~/vue_shared/translate';
+import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import Translate from '~/vue_shared/translate';
import RegistrySettingsApp from './components/registry_settings_app.vue';
import { apolloProvider } from './graphql/index';
diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
index 6fbae95094a..02929062cee 100644
--- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -1,8 +1,7 @@
<script>
import { GlFormGroup, GlFormRadioGroup, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-import RelatedIssuableInput from './related_issuable_input.vue';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import {
issuableTypesMap,
@@ -11,6 +10,7 @@ import {
addRelatedIssueErrorMap,
addRelatedItemErrorMap,
} from '../constants';
+import RelatedIssuableInput from './related_issuable_input.vue';
export default {
name: 'AddIssuableForm',
diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
index a124b055e19..2dc56c3110b 100644
--- a/app/assets/javascripts/related_issues/components/related_issuable_input.vue
+++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
@@ -1,13 +1,13 @@
<script>
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
-import issueToken from './issue_token.vue';
import {
autoCompleteTextMap,
inputPlaceholderConfidentialTextMap,
inputPlaceholderTextMap,
issuableTypesMap,
} from '../constants';
+import issueToken from './issue_token.vue';
const SPACE_FACTOR = 1;
diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue
index 2591e3e7f48..c042f0eef5f 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_block.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -1,13 +1,13 @@
<script>
import { GlLink, GlIcon, GlButton } from '@gitlab/ui';
-import AddIssuableForm from './add_issuable_form.vue';
-import RelatedIssuesList from './related_issues_list.vue';
import {
issuableIconMap,
issuableQaClassMap,
linkedIssueTypesMap,
linkedIssueTypesTextMap,
} from '../constants';
+import AddIssuableForm from './add_issuable_form.vue';
+import RelatedIssuesList from './related_issues_list.vue';
export default {
name: 'RelatedIssuesBlock',
@@ -146,7 +146,7 @@ export default {
class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
:aria-label="__('Read more about related issues')"
>
- <gl-icon name="question" :size="12" role="text" />
+ <gl-icon name="question" :size="12" />
</gl-link>
<div class="gl-display-inline-flex">
diff --git a/app/assets/javascripts/related_issues/components/related_issues_list.vue b/app/assets/javascripts/related_issues/components/related_issues_list.vue
index 8021d390d95..825a4a02b71 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_list.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue
@@ -60,8 +60,8 @@ export default {
const nextItemEl = itemEl.nextElementSibling;
return {
- beforeId: prevItemEl && parseInt(prevItemEl.dataset.orderingId, 0),
- afterId: nextItemEl && parseInt(nextItemEl.dataset.orderingId, 0),
+ beforeId: prevItemEl && parseInt(prevItemEl.dataset.orderingId, 10),
+ afterId: nextItemEl && parseInt(nextItemEl.dataset.orderingId, 10),
};
},
reordered(event) {
diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue
index a81edcf141c..c35a1ff0b63 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_root.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue
@@ -25,9 +25,6 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways:
*/
import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
-import RelatedIssuesBlock from './related_issues_block.vue';
-import RelatedIssuesStore from '../stores/related_issues_store';
-import RelatedIssuesService from '../services/related_issues_service';
import {
relatedIssuesRemoveErrorMap,
pathIndeterminateErrorMap,
@@ -35,6 +32,9 @@ import {
issuableTypesMap,
PathIdSeparator,
} from '../constants';
+import RelatedIssuesService from '../services/related_issues_service';
+import RelatedIssuesStore from '../stores/related_issues_store';
+import RelatedIssuesBlock from './related_issues_block.vue';
export default {
name: 'RelatedIssuesRoot',
diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
index 7db76ed576c..ccb92d2aedc 100644
--- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlLink, GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
import { sprintf, n__, s__ } from '~/locale';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import { parseIssuableData } from '../../issue_show/utils/parse_data';
diff --git a/app/assets/javascripts/related_merge_requests/store/actions.js b/app/assets/javascripts/related_merge_requests/store/actions.js
index 7baab165820..e9f0793a350 100644
--- a/app/assets/javascripts/related_merge_requests/store/actions.js
+++ b/app/assets/javascripts/related_merge_requests/store/actions.js
@@ -1,7 +1,7 @@
-import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
import * as types from './mutation_types';
const REQUEST_PAGE_COUNT = 100;
diff --git a/app/assets/javascripts/related_merge_requests/store/index.js b/app/assets/javascripts/related_merge_requests/store/index.js
index dcb70c22bcb..925cc36cd76 100644
--- a/app/assets/javascripts/related_merge_requests/store/index.js
+++ b/app/assets/javascripts/related_merge_requests/store/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
+import createState from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index 8d1bc44cba0..b16bb76c305 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -1,12 +1,12 @@
<script>
-import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup, GlSprintf } from '@gitlab/ui';
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { getParameterByName } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
-import { getParameterByName } from '~/lib/utils/common_utils';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import AssetLinksForm from './asset_links_form.vue';
-import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import TagField from './tag_field.vue';
export default {
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 5064b7dd6ad..32183e454c8 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -1,11 +1,11 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlEmptyState, GlLink, GlButton } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
import { getParameterByName } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import ReleaseBlock from './release_block.vue';
-import ReleasesPagination from './releases_pagination.vue';
import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
+import ReleasesPagination from './releases_pagination.vue';
import ReleasesSort from './releases_sort.vue';
export default {
diff --git a/app/assets/javascripts/releases/components/asset_links_form.vue b/app/assets/javascripts/releases/components/asset_links_form.vue
index 331cc8ade6c..9e095c8a9c2 100644
--- a/app/assets/javascripts/releases/components/asset_links_form.vue
+++ b/app/assets/javascripts/releases/components/asset_links_form.vue
@@ -1,5 +1,4 @@
<script>
-import { mapState, mapActions, mapGetters } from 'vuex';
import {
GlSprintf,
GlLink,
@@ -10,8 +9,9 @@ import {
GlFormInput,
GlFormSelect,
} from '@gitlab/ui';
-import { DEFAULT_ASSET_LINK_TYPE, ASSET_LINK_TYPE } from '../constants';
+import { mapState, mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
+import { DEFAULT_ASSET_LINK_TYPE, ASSET_LINK_TYPE } from '../constants';
export default {
name: 'AssetLinksForm',
diff --git a/app/assets/javascripts/releases/components/evidence_block.vue b/app/assets/javascripts/releases/components/evidence_block.vue
index 6e6017637d4..78831ceefe9 100644
--- a/app/assets/javascripts/releases/components/evidence_block.vue
+++ b/app/assets/javascripts/releases/components/evidence_block.vue
@@ -1,9 +1,9 @@
<script>
-import dateFormat from 'dateformat';
import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import { truncateSha } from '~/lib/utils/text_utility';
+import dateFormat from 'dateformat';
import { getTimeago } from '~/lib/utils/datetime_utility';
+import { truncateSha } from '~/lib/utils/text_utility';
+import { __, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index b89e5f2df3f..68bca2fc6b9 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -1,10 +1,10 @@
<script>
/* eslint-disable vue/no-v-html */
-import { isEmpty } from 'lodash';
import $ from 'jquery';
+import { isEmpty } from 'lodash';
+import { scrollToElement } from '~/lib/utils/common_utils';
import { slugify } from '~/lib/utils/text_utility';
import { getLocationHash } from '~/lib/utils/url_utility';
-import { scrollToElement } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import '~/behaviors/markdown/render_gfm';
import EvidenceBlock from './evidence_block.vue';
diff --git a/app/assets/javascripts/releases/components/release_block_assets.vue b/app/assets/javascripts/releases/components/release_block_assets.vue
index 36929f559b5..1761f4360d1 100644
--- a/app/assets/javascripts/releases/components/release_block_assets.vue
+++ b/app/assets/javascripts/releases/components/release_block_assets.vue
@@ -1,8 +1,8 @@
<script>
import { GlTooltipDirective, GlLink, GlButton, GlCollapse, GlIcon, GlBadge } from '@gitlab/ui';
import { difference, get } from 'lodash';
-import { ASSET_LINK_TYPE } from '../constants';
import { __, s__, sprintf } from '~/locale';
+import { ASSET_LINK_TYPE } from '../constants';
export default {
name: 'ReleaseBlockAssets',
diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue
index 3beec466c54..cb795b3cba7 100644
--- a/app/assets/javascripts/releases/components/release_block_footer.vue
+++ b/app/assets/javascripts/releases/components/release_block_footer.vue
@@ -1,8 +1,8 @@
<script>
import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { __, sprintf } from '~/locale';
export default {
name: 'ReleaseBlockFooter',
diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue
index 87538244f1a..356fc0f3bf3 100644
--- a/app/assets/javascripts/releases/components/release_block_header.vue
+++ b/app/assets/javascripts/releases/components/release_block_header.vue
@@ -1,7 +1,7 @@
<script>
-import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui';
-import { BACK_URL_PARAM } from '~/releases/constants';
+import { GlTooltipDirective, GlLink, GlBadge, GlButton, GlIcon } from '@gitlab/ui';
import { setUrlParams } from '~/lib/utils/url_utility';
+import { BACK_URL_PARAM } from '~/releases/constants';
export default {
name: 'ReleaseBlockHeader',
@@ -9,6 +9,7 @@ export default {
GlLink,
GlBadge,
GlButton,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -44,7 +45,19 @@ export default {
<gl-link v-if="selfLink" :href="selfLink" class="font-size-inherit">
{{ release.name }}
</gl-link>
- <template v-else>{{ release.name }}</template>
+ <template v-else>
+ {{ release.name }}
+ <gl-icon
+ v-gl-tooltip
+ name="lock"
+ :title="
+ __(
+ 'Private - Guest users are not allowed to view detailed release information like title and source code.',
+ )
+ "
+ class="text-secondary gl-mb-2"
+ />
+ </template>
<gl-badge v-if="release.upcomingRelease" variant="warning" class="align-middle">{{
__('Upcoming Release')
}}</gl-badge>
diff --git a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
index cb6f1fa18a1..7d024c47fb9 100644
--- a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
+++ b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlKeysetPagination } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
export default {
diff --git a/app/assets/javascripts/releases/components/releases_pagination_rest.vue b/app/assets/javascripts/releases/components/releases_pagination_rest.vue
index 334458a2302..24abb0f4498 100644
--- a/app/assets/javascripts/releases/components/releases_pagination_rest.vue
+++ b/app/assets/javascripts/releases/components/releases_pagination_rest.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapState } from 'vuex';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
export default {
name: 'ReleasesPaginationRest',
diff --git a/app/assets/javascripts/releases/components/tag_field_existing.vue b/app/assets/javascripts/releases/components/tag_field_existing.vue
index 046885fe2f6..3345bbecf6e 100644
--- a/app/assets/javascripts/releases/components/tag_field_existing.vue
+++ b/app/assets/javascripts/releases/components/tag_field_existing.vue
@@ -1,7 +1,7 @@
<script>
-import { mapState } from 'vuex';
-import { uniqueId } from 'lodash';
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { mapState } from 'vuex';
import FormFieldContainer from './form_field_container.vue';
export default {
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 4779feae886..660fd7ac950 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -1,7 +1,7 @@
<script>
-import { mapState, mapActions, mapGetters } from 'vuex';
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { uniqueId } from 'lodash';
+import { mapState, mapActions, mapGetters } from 'vuex';
import { __ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import FormFieldContainer from './form_field_container.vue';
diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js
index 127646826a6..5fa002706c6 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js
@@ -1,15 +1,15 @@
-import * as types from './mutation_types';
import api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
+import oneReleaseQuery from '~/releases/queries/one_release.query.graphql';
import {
releaseToApiJson,
apiJsonToRelease,
gqClient,
convertOneReleaseGraphQLResponse,
} from '~/releases/util';
-import oneReleaseQuery from '~/releases/queries/one_release.query.graphql';
+import * as types from './mutation_types';
export const initializeRelease = ({ commit, dispatch, getters }) => {
if (getters.isExistingRelease) {
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutations.js b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
index 8f4bfbc9b86..cf282f9ab2c 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
@@ -1,6 +1,6 @@
import { uniqueId, cloneDeep } from 'lodash';
-import * as types from './mutation_types';
import { DEFAULT_ASSET_LINK_TYPE } from '../../../constants';
+import * as types from './mutation_types';
const findReleaseLink = (release, id) => {
return release.assets.links.find((l) => l.id === id);
diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/list/actions.js
index 4c4f6e19a93..f1add54626a 100644
--- a/app/assets/javascripts/releases/stores/modules/list/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/list/actions.js
@@ -1,15 +1,15 @@
-import * as types from './mutation_types';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { __ } from '~/locale';
import api from '~/api';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
normalizeHeaders,
parseIntPagination,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
-import { gqClient, convertAllReleasesGraphQLResponse } from '../../../util';
import { PAGE_SIZE } from '../../../constants';
+import { gqClient, convertAllReleasesGraphQLResponse } from '../../../util';
+import * as types from './mutation_types';
/**
* Gets a paginated list of releases from the server
diff --git a/app/assets/javascripts/releases/stores/modules/list/index.js b/app/assets/javascripts/releases/stores/modules/list/index.js
index 244f41b6609..d5ca191153a 100644
--- a/app/assets/javascripts/releases/stores/modules/list/index.js
+++ b/app/assets/javascripts/releases/stores/modules/list/index.js
@@ -1,6 +1,6 @@
-import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
+import createState from './state';
export default (initialState) => ({
namespaced: true,
diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js
index b24a226cf9c..36c17b5b252 100644
--- a/app/assets/javascripts/releases/util.js
+++ b/app/assets/javascripts/releases/util.js
@@ -1,10 +1,10 @@
import { pick } from 'lodash';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
-import { truncateSha } from '~/lib/utils/text_utility';
import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
} from '~/lib/utils/common_utils';
+import { truncateSha } from '~/lib/utils/text_utility';
/**
* Converts a release object into a JSON object that can sent to the public
diff --git a/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue b/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
index 6f8ddd01df8..c272e3b1dc4 100644
--- a/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
+++ b/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { componentNames } from '~/reports/components/issue_body';
-import ReportSection from '~/reports/components/report_section.vue';
import IssuesList from '~/reports/components/issues_list.vue';
+import ReportSection from '~/reports/components/report_section.vue';
import createStore from './store';
export default {
diff --git a/app/assets/javascripts/reports/accessibility_report/store/actions.js b/app/assets/javascripts/reports/accessibility_report/store/actions.js
index bb502020a06..e0142a35291 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/actions.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/actions.js
@@ -1,7 +1,7 @@
import Visibility from 'visibilityjs';
-import Poll from '~/lib/utils/poll';
-import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
+import httpStatusCodes from '~/lib/utils/http_status';
+import Poll from '~/lib/utils/poll';
import * as types from './mutation_types';
let eTagPoll;
diff --git a/app/assets/javascripts/reports/accessibility_report/store/getters.js b/app/assets/javascripts/reports/accessibility_report/store/getters.js
index 8f8eec11c7f..20506b1bfd1 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/getters.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/getters.js
@@ -1,5 +1,5 @@
-import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants';
import { s__, n__ } from '~/locale';
+import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants';
export const groupedSummaryText = (state) => {
if (state.isLoading) {
diff --git a/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue b/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue
index d0a5615bb57..e5980f1e539 100644
--- a/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue
+++ b/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue
@@ -4,9 +4,9 @@
* Fixed: [name] in [link]:[line]
*/
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
import ReportLink from '~/reports/components/report_link.vue';
import { STATUS_SUCCESS } from '~/reports/constants';
-import { s__ } from '~/locale';
import { SEVERITY_CLASSES, SEVERITY_ICONS } from '../constants';
export default {
diff --git a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
index 5c8f31d7da0..654508f0736 100644
--- a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
+++ b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
@@ -1,8 +1,9 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import { componentNames } from '~/reports/components/issue_body';
import { s__, sprintf } from '~/locale';
+import { componentNames } from '~/reports/components/issue_body';
import ReportSection from '~/reports/components/report_section.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import createStore from './store';
export default {
@@ -11,6 +12,7 @@ export default {
components: {
ReportSection,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
headPath: {
type: String,
@@ -30,6 +32,11 @@ export default {
required: false,
default: null,
},
+ codequalityReportsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
codequalityHelpPath: {
type: String,
required: true,
@@ -37,7 +44,7 @@ export default {
},
componentNames,
computed: {
- ...mapState(['newIssues', 'resolvedIssues']),
+ ...mapState(['newIssues', 'resolvedIssues', 'hasError', 'statusReason']),
...mapGetters([
'hasCodequalityIssues',
'codequalityStatus',
@@ -51,10 +58,11 @@ export default {
headPath: this.headPath,
baseBlobPath: this.baseBlobPath,
headBlobPath: this.headBlobPath,
+ reportsPath: this.codequalityReportsPath,
helpPath: this.codequalityHelpPath,
});
- this.fetchReports();
+ this.fetchReports(this.glFeatures.codequalityBackendComparison);
},
methods: {
...mapActions(['fetchReports', 'setPaths']),
@@ -80,5 +88,7 @@ export default {
:popover-options="codequalityPopover"
:show-report-section-status-icon="false"
class="js-codequality-widget mr-widget-border-top mr-report"
- />
+ >
+ <template v-if="hasError" #sub-heading>{{ statusReason }}</template>
+ </report-section>
</template>
diff --git a/app/assets/javascripts/reports/codequality_report/store/actions.js b/app/assets/javascripts/reports/codequality_report/store/actions.js
index e5fb5caca2e..ddd1747899f 100644
--- a/app/assets/javascripts/reports/codequality_report/store/actions.js
+++ b/app/assets/javascripts/reports/codequality_report/store/actions.js
@@ -4,9 +4,20 @@ import { parseCodeclimateMetrics, doCodeClimateComparison } from './utils/codequ
export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
-export const fetchReports = ({ state, dispatch, commit }) => {
+export const fetchReports = ({ state, dispatch, commit }, diffFeatureFlagEnabled) => {
commit(types.REQUEST_REPORTS);
+ if (diffFeatureFlagEnabled) {
+ return axios
+ .get(state.reportsPath)
+ .then(({ data }) => {
+ return dispatch('receiveReportsSuccess', {
+ newIssues: parseCodeclimateMetrics(data.new_errors, state.headBlobPath),
+ resolvedIssues: parseCodeclimateMetrics(data.resolved_errors, state.baseBlobPath),
+ });
+ })
+ .catch((error) => dispatch('receiveReportsError', error));
+ }
if (!state.basePath) {
return dispatch('receiveReportsError');
}
@@ -18,13 +29,13 @@ export const fetchReports = ({ state, dispatch, commit }) => {
),
)
.then((data) => dispatch('receiveReportsSuccess', data))
- .catch(() => dispatch('receiveReportsError'));
+ .catch((error) => dispatch('receiveReportsError', error));
};
export const receiveReportsSuccess = ({ commit }, data) => {
commit(types.RECEIVE_REPORTS_SUCCESS, data);
};
-export const receiveReportsError = ({ commit }) => {
- commit(types.RECEIVE_REPORTS_ERROR);
+export const receiveReportsError = ({ commit }, error) => {
+ commit(types.RECEIVE_REPORTS_ERROR, error);
};
diff --git a/app/assets/javascripts/reports/codequality_report/store/getters.js b/app/assets/javascripts/reports/codequality_report/store/getters.js
index e017bab976c..c6935291af2 100644
--- a/app/assets/javascripts/reports/codequality_report/store/getters.js
+++ b/app/assets/javascripts/reports/codequality_report/store/getters.js
@@ -1,6 +1,6 @@
-import { LOADING, ERROR, SUCCESS } from '../../constants';
-import { sprintf, __, s__, n__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils';
+import { sprintf, __, s__, n__ } from '~/locale';
+import { LOADING, ERROR, SUCCESS } from '../../constants';
export const hasCodequalityIssues = (state) =>
Boolean(state.newIssues?.length || state.resolvedIssues?.length);
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutations.js b/app/assets/javascripts/reports/codequality_report/store/mutations.js
index 7ef4f3ce2db..095e6637966 100644
--- a/app/assets/javascripts/reports/codequality_report/store/mutations.js
+++ b/app/assets/javascripts/reports/codequality_report/store/mutations.js
@@ -6,6 +6,7 @@ export default {
state.headPath = paths.headPath;
state.baseBlobPath = paths.baseBlobPath;
state.headBlobPath = paths.headBlobPath;
+ state.reportsPath = paths.reportsPath;
state.helpPath = paths.helpPath;
},
[types.REQUEST_REPORTS](state) {
@@ -13,12 +14,14 @@ export default {
},
[types.RECEIVE_REPORTS_SUCCESS](state, data) {
state.hasError = false;
+ state.statusReason = '';
state.isLoading = false;
state.newIssues = data.newIssues;
state.resolvedIssues = data.resolvedIssues;
},
- [types.RECEIVE_REPORTS_ERROR](state) {
+ [types.RECEIVE_REPORTS_ERROR](state, error) {
state.isLoading = false;
state.hasError = true;
+ state.statusReason = error?.response?.data?.status_reason;
},
};
diff --git a/app/assets/javascripts/reports/codequality_report/store/state.js b/app/assets/javascripts/reports/codequality_report/store/state.js
index 38ab53b432e..b39ff4f9d66 100644
--- a/app/assets/javascripts/reports/codequality_report/store/state.js
+++ b/app/assets/javascripts/reports/codequality_report/store/state.js
@@ -1,12 +1,14 @@
export default () => ({
basePath: null,
headPath: null,
+ reportsPath: null,
baseBlobPath: null,
headBlobPath: null,
isLoading: false,
hasError: false,
+ statusReason: '',
newIssues: [],
resolvedIssues: [],
diff --git a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
index fd775f52f7d..b252c8c9817 100644
--- a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
+++ b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
@@ -3,8 +3,10 @@ import CodeQualityComparisonWorker from '../../workers/codequality_comparison_wo
export const parseCodeclimateMetrics = (issues = [], path = '') => {
return issues.map((issue) => {
const parsedIssue = {
- ...issue,
name: issue.description,
+ path: issue.file_path,
+ urlPath: `${path}/${issue.file_path}#L${issue.line}`,
+ ...issue,
};
if (issue?.location?.path) {
diff --git a/app/assets/javascripts/reports/components/grouped_issues_list.vue b/app/assets/javascripts/reports/components/grouped_issues_list.vue
index 1826fbaddac..4d3c5f48e94 100644
--- a/app/assets/javascripts/reports/components/grouped_issues_list.vue
+++ b/app/assets/javascripts/reports/components/grouped_issues_list.vue
@@ -1,7 +1,7 @@
<script>
import { s__ } from '~/locale';
-import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import ReportItem from '~/reports/components/report_item.vue';
+import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
components: {
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
index bf1868d427e..033b8798473 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -1,21 +1,21 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
-import { once } from 'lodash';
import { GlButton } from '@gitlab/ui';
+import { once } from 'lodash';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { sprintf, s__ } from '~/locale';
-import { componentNames } from './issue_body';
-import ReportSection from './report_section.vue';
-import SummaryRow from './summary_row.vue';
-import IssuesList from './issues_list.vue';
-import Modal from './modal.vue';
-import createStore from '../store';
import Tracking from '~/tracking';
+import createStore from '../store';
import {
summaryTextBuilder,
reportTextBuilder,
statusIcon,
recentFailuresTextBuilder,
} from '../store/utils';
+import { componentNames } from './issue_body';
+import IssuesList from './issues_list.vue';
+import Modal from './modal.vue';
+import ReportSection from './report_section.vue';
+import SummaryRow from './summary_row.vue';
export default {
name: 'GroupedTestReportsApp',
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js
index 1e6dc4f8b78..a0349506b69 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/reports/components/issue_body.js
@@ -1,6 +1,6 @@
-import TestIssueBody from './test_issue_body.vue';
import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
import CodequalityIssueBody from '../codequality_report/components/codequality_issue_body.vue';
+import TestIssueBody from './test_issue_body.vue';
export const components = {
AccessibilityIssueBody,
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index 0e9975ea81f..9d0631fbc01 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -2,8 +2,8 @@
import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
-import IssuesList from './issues_list.vue';
import { status, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '../constants';
+import IssuesList from './issues_list.vue';
export default {
name: 'ReportSection',
diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue
index ad980b334bb..7508e1d1f0d 100644
--- a/app/assets/javascripts/reports/components/test_issue_body.vue
+++ b/app/assets/javascripts/reports/components/test_issue_body.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions } from 'vuex';
import { GlBadge, GlSprintf } from '@gitlab/ui';
+import { mapActions } from 'vuex';
export default {
name: 'TestIssueBody',
diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js
index 301fdce7989..c9b8ee64930 100644
--- a/app/assets/javascripts/reports/store/actions.js
+++ b/app/assets/javascripts/reports/store/actions.js
@@ -1,8 +1,8 @@
import Visibility from 'visibilityjs';
import axios from '../../lib/utils/axios_utils';
+import httpStatusCodes from '../../lib/utils/http_status';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
-import httpStatusCodes from '../../lib/utils/http_status';
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index c9862572b16..28d7dec85f4 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -10,8 +10,8 @@ import permissionsQuery from 'shared_queries/repository/permissions.query.graphq
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '../../locale';
import getRefMixin from '../mixins/get_ref';
-import projectShortPathQuery from '../queries/project_short_path.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
+import projectShortPathQuery from '../queries/project_short_path.query.graphql';
const ROW_TYPES = {
header: 'header',
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 0241c803514..a7176853819 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -4,10 +4,10 @@ import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } fr
import defaultAvatarUrl from 'images/no_avatar.png';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
-import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
+import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index e2c3f3b81ee..b74c2333148 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { handleLocationHash } from '~/lib/utils/common_utils';
import readmeQuery from '../../queries/readme.query.graphql';
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index c6652c57c1f..22dffb7d2db 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -4,8 +4,8 @@ import { sprintf, __ } from '../../../locale';
import getRefMixin from '../../mixins/get_ref';
import projectPathQuery from '../../queries/project_path.query.graphql';
import TableHeader from './header.vue';
-import TableRow from './row.vue';
import ParentRow from './parent_row.vue';
+import TableRow from './row.vue';
export default {
components: {
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index d749a8c0dee..70918dd55e4 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,6 +1,5 @@
<script>
/* eslint-disable vue/no-v-html */
-import { escapeRegExp } from 'lodash';
import {
GlBadge,
GlLink,
@@ -9,9 +8,10 @@ import {
GlLoadingIcon,
GlIcon,
} from '@gitlab/ui';
+import { escapeRegExp } from 'lodash';
import { escapeFileUrl } from '~/lib/utils/url_utility';
-import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import getRefMixin from '../../mixins/get_ref';
import commitQuery from '../../queries/commit.query.graphql';
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 7fe6863d006..336237abd8a 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -2,11 +2,11 @@
import filesQuery from 'shared_queries/repository/files.query.graphql';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '../../locale';
-import FileTable from './table/index.vue';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
-import FilePreview from './preview/index.vue';
import { readmeFile } from '../utils/readme';
+import FilePreview from './preview/index.vue';
+import FileTable from './table/index.vue';
const LIMIT = 1000;
const PAGE_SIZE = 100;
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index dbe129859bc..4a4b9d115b7 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -1,8 +1,8 @@
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
-import axios from '~/lib/utils/axios_utils';
import createDefaultClient from '~/lib/graphql';
+import axios from '~/lib/utils/axios_utils';
import introspectionQueryResultData from './fragmentTypes.json';
import { fetchLogsTree } from './log_tree';
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index f56b141fe5c..747b85f5c1c 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -1,17 +1,17 @@
import Vue from 'vue';
+import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
+import { parseBoolean } from '../lib/utils/common_utils';
import { escapeFileUrl } from '../lib/utils/url_utility';
-import createRouter from './router';
+import { __ } from '../locale';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
+import DirectoryDownloadLinks from './components/directory_download_links.vue';
import LastCommit from './components/last_commit.vue';
import TreeActionLink from './components/tree_action_link.vue';
-import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
-import DirectoryDownloadLinks from './components/directory_download_links.vue';
import apolloProvider from './graphql';
-import { setTitle } from './utils/title';
+import createRouter from './router';
import { updateFormAction } from './utils/dom';
-import { parseBoolean } from '../lib/utils/common_utils';
-import { __ } from '../locale';
+import { setTitle } from './utils/title';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
diff --git a/app/assets/javascripts/repository/mixins/preload.js b/app/assets/javascripts/repository/mixins/preload.js
index c1607866941..ffc260ec84f 100644
--- a/app/assets/javascripts/repository/mixins/preload.js
+++ b/app/assets/javascripts/repository/mixins/preload.js
@@ -1,6 +1,6 @@
import filesQuery from 'shared_queries/repository/files.query.graphql';
-import getRefMixin from './get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
+import getRefMixin from './get_ref';
export default {
mixins: [getRefMixin],
diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue
index 29786bf4ec8..0e53235779c 100644
--- a/app/assets/javascripts/repository/pages/index.vue
+++ b/app/assets/javascripts/repository/pages/index.vue
@@ -1,6 +1,6 @@
<script>
-import TreePage from './tree.vue';
import { updateElementsVisibility } from '../utils/dom';
+import TreePage from './tree.vue';
export default {
components: {
diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue
index adc332fa370..cbdc62624d4 100644
--- a/app/assets/javascripts/repository/pages/tree.vue
+++ b/app/assets/javascripts/repository/pages/tree.vue
@@ -1,7 +1,7 @@
<script>
import TreeContent from '../components/tree_content.vue';
-import { updateElementsVisibility } from '../utils/dom';
import preloadMixin from '../mixins/preload';
+import { updateElementsVisibility } from '../utils/dom';
export default {
components: {
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index e2924454239..ad6e32d7055 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -1,6 +1,6 @@
+import { escapeRegExp } from 'lodash';
import Vue from 'vue';
import VueRouter from 'vue-router';
-import { escapeRegExp } from 'lodash';
import { joinPaths } from '../lib/utils/url_utility';
import IndexPage from './pages/index.vue';
import TreePage from './pages/tree.vue';
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index b9bc799fb0b..52ff4e7b100 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -2,10 +2,10 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
+import { fixTitle, hide } from '~/tooltips';
import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
-import { fixTitle, hide } from '~/tooltips';
function Sidebar() {
this.toggleTodo = this.toggleTodo.bind(this);
@@ -123,7 +123,7 @@ Sidebar.prototype.todoUpdateDone = function (data) {
.data('deletePath', deletePath);
if ($el.hasClass('has-tooltip')) {
- fixTitle($el);
+ fixTitle(el);
}
if (typeof $el.data('isCollapsed') !== 'undefined') {
diff --git a/app/assets/javascripts/search/highlight_blob_search_result.js b/app/assets/javascripts/search/highlight_blob_search_result.js
index 3c3ac3582d0..c553d5b14a0 100644
--- a/app/assets/javascripts/search/highlight_blob_search_result.js
+++ b/app/assets/javascripts/search/highlight_blob_search_result.js
@@ -1,7 +1,7 @@
-export default () => {
+export default (search = '') => {
const highlightLineClass = 'hll';
const contentBody = document.getElementById('content-body');
- const searchTerm = contentBody.querySelector('.js-search-input').value.toLowerCase();
+ const searchTerm = search.toLowerCase();
const blobs = contentBody.querySelectorAll('.blob-result');
blobs.forEach((blob) => {
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index d2bb1ccfc44..10c41315972 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -1,14 +1,25 @@
+import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import { queryToObject } from '~/lib/utils/url_utility';
+import Project from '~/pages/projects/project';
+import refreshCounts from '~/pages/search/show/refresh_counts';
+import { initSidebar } from './sidebar';
+import { initSearchSort } from './sort';
import createStore from './store';
import { initTopbar } from './topbar';
-import { initSidebar } from './sidebar';
export const initSearchApp = () => {
// Similar to url_utility.decodeUrlParameter
// Our query treats + as %20. This replaces the query + symbols with %20.
const sanitizedSearch = window.location.search.replace(/\+/g, '%20');
- const store = createStore({ query: queryToObject(sanitizedSearch) });
+ const query = queryToObject(sanitizedSearch);
+
+ const store = createStore({ query });
initTopbar(store);
initSidebar(store);
+ initSearchSort(store);
+
+ setHighlightClass(query.search); // Code Highlighting
+ refreshCounts(); // Other Scope Tab Counts
+ Project.initRefSwitcher(); // Code Search Branch Picker
};
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index e233d18b716..4640259314b 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -1,8 +1,8 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlButton, GlLink } from '@gitlab/ui';
-import StatusFilter from './status_filter.vue';
+import { mapActions, mapState } from 'vuex';
import ConfidentialityFilter from './confidentiality_filter.vue';
+import StatusFilter from './status_filter.vue';
export default {
name: 'GlobalSearchSidebar',
diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
index b27c4e26fb5..73911b9d319 100644
--- a/app/assets/javascripts/search/sidebar/components/radio_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
import { sprintf, s__ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/search/sort/components/app.vue b/app/assets/javascripts/search/sort/components/app.vue
new file mode 100644
index 00000000000..e4eba655e39
--- /dev/null
+++ b/app/assets/javascripts/search/sort/components/app.vue
@@ -0,0 +1,103 @@
+<script>
+import {
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { SORT_DIRECTION_UI } from '../constants';
+
+export default {
+ name: 'GlobalSearchSort',
+ components: {
+ GlButtonGroup,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ searchSortOptions: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['query']),
+ selectedSortOption: {
+ get() {
+ const { sort } = this.query;
+
+ if (!sort) {
+ return this.searchSortOptions[0];
+ }
+
+ const sortOption = this.searchSortOptions.find((option) => {
+ if (!option.sortable) {
+ return option.sortParam === sort;
+ }
+
+ return Object.values(option.sortParam).indexOf(sort) !== -1;
+ });
+
+ // Handle invalid sort param
+ return sortOption || this.searchSortOptions[0];
+ },
+ set(value) {
+ this.setQuery({ key: 'sort', value });
+ this.applyQuery();
+ },
+ },
+ sortDirectionData() {
+ if (!this.selectedSortOption.sortable) {
+ return SORT_DIRECTION_UI.disabled;
+ }
+
+ return this.query?.sort?.includes('asc') ? SORT_DIRECTION_UI.asc : SORT_DIRECTION_UI.desc;
+ },
+ },
+ methods: {
+ ...mapActions(['applyQuery', 'setQuery']),
+ handleSortChange(option) {
+ if (!option.sortable) {
+ this.selectedSortOption = option.sortParam;
+ } else {
+ // Default new sort options to desc
+ this.selectedSortOption = option.sortParam.desc;
+ }
+ },
+ handleSortDirectionChange() {
+ this.selectedSortOption =
+ this.sortDirectionData.direction === 'desc'
+ ? this.selectedSortOption.sortParam.asc
+ : this.selectedSortOption.sortParam.desc;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button-group>
+ <gl-dropdown :text="selectedSortOption.title" :right="true" class="w-100">
+ <gl-dropdown-item
+ v-for="sortOption in searchSortOptions"
+ :key="sortOption.title"
+ is-check-item
+ :is-checked="sortOption.title === selectedSortOption.title"
+ @click="handleSortChange(sortOption)"
+ >{{ sortOption.title }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+ <gl-button
+ v-gl-tooltip
+ :disabled="!selectedSortOption.sortable"
+ :title="sortDirectionData.tooltip"
+ :icon="sortDirectionData.icon"
+ @click="handleSortDirectionChange"
+ />
+ </gl-button-group>
+</template>
diff --git a/app/assets/javascripts/search/sort/constants.js b/app/assets/javascripts/search/sort/constants.js
new file mode 100644
index 00000000000..575fba5873b
--- /dev/null
+++ b/app/assets/javascripts/search/sort/constants.js
@@ -0,0 +1,19 @@
+import { __ } from '~/locale';
+
+export const SORT_DIRECTION_UI = {
+ disabled: {
+ direction: null,
+ tooltip: '',
+ icon: 'sort-highest',
+ },
+ desc: {
+ direction: 'desc',
+ tooltip: __('Sort direction: Descending'),
+ icon: 'sort-highest',
+ },
+ asc: {
+ direction: 'asc',
+ tooltip: __('Sort direction: Ascending'),
+ icon: 'sort-lowest',
+ },
+};
diff --git a/app/assets/javascripts/search/sort/index.js b/app/assets/javascripts/search/sort/index.js
new file mode 100644
index 00000000000..84bb5175b1d
--- /dev/null
+++ b/app/assets/javascripts/search/sort/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import GlobalSearchSort from './components/app.vue';
+
+Vue.use(Translate);
+
+export const initSearchSort = (store) => {
+ const el = document.getElementById('js-search-sort');
+
+ if (!el) return false;
+
+ let { searchSortOptions } = el.dataset;
+
+ searchSortOptions = JSON.parse(searchSortOptions);
+
+ return new Vue({
+ el,
+ store,
+ render(createElement) {
+ return createElement(GlobalSearchSort, {
+ props: {
+ searchSortOptions,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index bdfe966d990..0af679644f3 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -1,7 +1,7 @@
import Api from '~/api';
import createFlash from '~/flash';
-import { __ } from '~/locale';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import * as types from './mutation_types';
export const fetchGroups = ({ commit }, search) => {
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
new file mode 100644
index 00000000000..987735ed811
--- /dev/null
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import GroupFilter from './group_filter.vue';
+import ProjectFilter from './project_filter.vue';
+
+export default {
+ name: 'GlobalSearchTopbar',
+ components: {
+ GlForm,
+ GlSearchBoxByType,
+ GroupFilter,
+ ProjectFilter,
+ GlButton,
+ },
+ props: {
+ groupInitialData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ projectInitialData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ ...mapState(['query']),
+ search: {
+ get() {
+ return this.query ? this.query.search : '';
+ },
+ set(value) {
+ this.setQuery({ key: 'search', value });
+ },
+ },
+ showFilters() {
+ return !this.query.snippets || this.query.snippets === 'false';
+ },
+ },
+ methods: {
+ ...mapActions(['applyQuery', 'setQuery']),
+ },
+};
+</script>
+
+<template>
+ <gl-form class="search-page-form" @submit.prevent="applyQuery">
+ <section class="gl-lg-display-flex gl-align-items-flex-end">
+ <div class="gl-flex-fill-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
+ <label>{{ __('What are you searching for?') }}</label>
+ <gl-search-box-by-type
+ id="dashboard_search"
+ v-model="search"
+ name="search"
+ :placeholder="__(`Search for projects, issues, etc.`)"
+ />
+ </div>
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
+ <label class="gl-display-block">{{ __('Group') }}</label>
+ <group-filter :initial-data="groupInitialData" />
+ </div>
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
+ <label class="gl-display-block">{{ __('Project') }}</label>
+ <project-filter :initial-data="projectInitialData" />
+ </div>
+ <gl-button class="btn-search gl-lg-ml-2" variant="success" type="submit">{{
+ __('Search')
+ }}</gl-button>
+ </section>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/topbar/components/group_filter.vue
index fce9ec17d23..2acab4e805d 100644
--- a/app/assets/javascripts/search/topbar/components/group_filter.vue
+++ b/app/assets/javascripts/search/topbar/components/group_filter.vue
@@ -1,9 +1,9 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
+import { mapState, mapActions } from 'vuex';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
-import SearchableDropdown from './searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
+import SearchableDropdown from './searchable_dropdown.vue';
export default {
name: 'GroupFilter',
@@ -37,6 +37,7 @@ export default {
<template>
<searchable-dropdown
+ data-testid="group-filter"
:header-text="$options.GROUP_DATA.headerText"
:selected-display-value="$options.GROUP_DATA.selectedDisplayValue"
:items-display-value="$options.GROUP_DATA.itemsDisplayValue"
diff --git a/app/assets/javascripts/search/topbar/components/project_filter.vue b/app/assets/javascripts/search/topbar/components/project_filter.vue
index 3f1f3848ac7..b2dd79fcfa3 100644
--- a/app/assets/javascripts/search/topbar/components/project_filter.vue
+++ b/app/assets/javascripts/search/topbar/components/project_filter.vue
@@ -1,8 +1,8 @@
<script>
import { mapState, mapActions } from 'vuex';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
-import SearchableDropdown from './searchable_dropdown.vue';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
+import SearchableDropdown from './searchable_dropdown.vue';
export default {
name: 'ProjectFilter',
@@ -27,7 +27,7 @@ export default {
handleProjectChange(project) {
// This determines if we need to update the group filter or not
const queryParams = {
- ...(project.namespace_id && { [GROUP_DATA.queryParam]: project.namespace_id }),
+ ...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }),
[PROJECT_DATA.queryParam]: project.id,
};
@@ -40,6 +40,7 @@ export default {
<template>
<searchable-dropdown
+ data-testid="project-filter"
:header-text="$options.PROJECT_DATA.headerText"
:selected-display-value="$options.PROJECT_DATA.selectedDisplayValue"
:items-display-value="$options.PROJECT_DATA.itemsDisplayValue"
diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
index 14577fd7d7a..5fb7217db74 100644
--- a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
+++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
@@ -101,7 +101,7 @@ export default {
@keydown.enter.stop="resetDropdown"
@click.stop="resetDropdown"
>
- <gl-icon name="clear" class="gl-text-gray-200! gl-hover-text-blue-800!" />
+ <gl-icon name="clear" />
</gl-button>
<gl-icon name="chevron-down" />
</template>
diff --git a/app/assets/javascripts/search/topbar/index.js b/app/assets/javascripts/search/topbar/index.js
index f0308109b32..87316e10e8d 100644
--- a/app/assets/javascripts/search/topbar/index.js
+++ b/app/assets/javascripts/search/topbar/index.js
@@ -1,44 +1,31 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
-import GroupFilter from './components/group_filter.vue';
-import ProjectFilter from './components/project_filter.vue';
+import GlobalSearchTopbar from './components/app.vue';
Vue.use(Translate);
-const mountSearchableDropdown = (store, { id, component }) => {
- const el = document.getElementById(id);
+export const initTopbar = (store) => {
+ const el = document.getElementById('js-search-topbar');
if (!el) {
return false;
}
- let { initialData } = el.dataset;
+ let { groupInitialData, projectInitialData } = el.dataset;
- initialData = JSON.parse(initialData);
+ groupInitialData = JSON.parse(groupInitialData);
+ projectInitialData = JSON.parse(projectInitialData);
return new Vue({
el,
store,
render(createElement) {
- return createElement(component, {
+ return createElement(GlobalSearchTopbar, {
props: {
- initialData,
+ groupInitialData,
+ projectInitialData,
},
});
},
});
};
-
-const searchableDropdowns = [
- {
- id: 'js-search-group-dropdown',
- component: GroupFilter,
- },
- {
- id: 'js-search-project-dropdown',
- component: ProjectFilter,
- },
-];
-
-export const initTopbar = (store) =>
- searchableDropdowns.map((dropdown) => mountSearchableDropdown(store, dropdown));
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index b8a5836e2d4..9c133a79607 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -2,8 +2,10 @@
import $ from 'jquery';
import { escape, throttle } from 'lodash';
-import { s__, __, sprintf } from '~/locale';
+import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
+import { s__, __, sprintf } from '~/locale';
+import Tracking from '~/tracking';
import axios from './lib/utils/axios_utils';
import {
isInGroupsPage,
@@ -12,8 +14,6 @@ import {
getProjectSlug,
spriteIcon,
} from './lib/utils/common_utils';
-import Tracking from '~/tracking';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
/**
* Search input in top navigation bar.
diff --git a/app/assets/javascripts/search_settings/components/search_settings.vue b/app/assets/javascripts/search_settings/components/search_settings.vue
index 820055dc656..116967a62c8 100644
--- a/app/assets/javascripts/search_settings/components/search_settings.vue
+++ b/app/assets/javascripts/search_settings/components/search_settings.vue
@@ -3,20 +3,37 @@ import { GlSearchBoxByType } from '@gitlab/ui';
import { uniq } from 'lodash';
import { EXCLUDED_NODES, HIDE_CLASS, HIGHLIGHT_CLASS, TYPING_DELAY } from '../constants';
+const origExpansions = new Map();
+
const findSettingsSection = (sectionSelector, node) => {
return node.parentElement.closest(sectionSelector);
};
-const resetSections = ({ sectionSelector, expandSection, collapseSection }) => {
- document.querySelectorAll(sectionSelector).forEach((section, index) => {
- section.classList.remove(HIDE_CLASS);
-
- if (index === 0) {
+const restoreExpansionState = ({ expandSection, collapseSection }) => {
+ origExpansions.forEach((isExpanded, section) => {
+ if (isExpanded) {
expandSection(section);
} else {
collapseSection(section);
}
});
+
+ origExpansions.clear();
+};
+
+const saveExpansionState = (sections, { isExpanded }) => {
+ // If we've saved expansions before, don't override it.
+ if (origExpansions.size > 0) {
+ return;
+ }
+
+ sections.forEach((section) => origExpansions.set(section, isExpanded(section)));
+};
+
+const resetSections = ({ sectionSelector }) => {
+ document.querySelectorAll(sectionSelector).forEach((section) => {
+ section.classList.remove(HIDE_CLASS);
+ });
};
const clearHighlights = () => {
@@ -85,6 +102,12 @@ export default {
type: String,
required: true,
},
+ isExpandedFn: {
+ type: Function,
+ required: false,
+ // default to a function that returns false
+ default: () => () => false,
+ },
},
data() {
return {
@@ -97,6 +120,7 @@ export default {
sectionSelector: this.sectionSelector,
expandSection: this.expandSection,
collapseSection: this.collapseSection,
+ isExpanded: this.isExpandedFn,
};
this.searchTerm = value;
@@ -104,7 +128,11 @@ export default {
clearResults(displayOptions);
if (value.length) {
+ saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions);
+
displayResults(displayOptions, search(this.searchRoot, value));
+ } else {
+ restoreExpansionState(displayOptions);
}
},
expandSection(section) {
@@ -118,12 +146,10 @@ export default {
};
</script>
<template>
- <div class="gl-mt-5">
- <gl-search-box-by-type
- :value="searchTerm"
- :debounce="$options.TYPING_DELAY"
- :placeholder="__('Search settings')"
- @input="search"
- />
- </div>
+ <gl-search-box-by-type
+ :value="searchTerm"
+ :debounce="$options.TYPING_DELAY"
+ :placeholder="__('Search settings')"
+ @input="search"
+ />
</template>
diff --git a/app/assets/javascripts/search_settings/index.js b/app/assets/javascripts/search_settings/index.js
index 1fb1a378ffb..676c43c5631 100644
--- a/app/assets/javascripts/search_settings/index.js
+++ b/app/assets/javascripts/search_settings/index.js
@@ -1,23 +1,10 @@
-import Vue from 'vue';
-import $ from 'jquery';
-import { expandSection, closeSection } from '~/settings_panels';
-import SearchSettings from '~/search_settings/components/search_settings.vue';
+const initSearch = async () => {
+ const el = document.querySelector('.js-search-settings-app');
-const initSearch = ({ el }) =>
- new Vue({
- el,
- render: (h) =>
- h(SearchSettings, {
- ref: 'searchSettings',
- props: {
- searchRoot: document.querySelector('#content-body'),
- sectionSelector: 'section.settings',
- },
- on: {
- collapse: (section) => closeSection($(section)),
- expand: (section) => expandSection($(section)),
- },
- }),
- });
+ if (el) {
+ const { default: mount } = await import(/* webpackChunkName: 'search_settings' */ './mount');
+ mount({ el });
+ }
+};
export default initSearch;
diff --git a/app/assets/javascripts/search_settings/mount.js b/app/assets/javascripts/search_settings/mount.js
new file mode 100644
index 00000000000..b1086f9ca1f
--- /dev/null
+++ b/app/assets/javascripts/search_settings/mount.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import SearchSettings from '~/search_settings/components/search_settings.vue';
+import { expandSection, closeSection, isExpanded } from '~/settings_panels';
+
+const mountSearch = ({ el }) =>
+ new Vue({
+ el,
+ render: (h) =>
+ h(SearchSettings, {
+ ref: 'searchSettings',
+ props: {
+ searchRoot: document.querySelector('#content-body'),
+ sectionSelector: '.js-search-settings-section, section.settings',
+ isExpandedFn: isExpanded,
+ },
+ on: {
+ collapse: closeSection,
+ expand: expandSection,
+ },
+ }),
+ });
+
+export default mountSearch;
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
new file mode 100644
index 00000000000..513a7353d28
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -0,0 +1,23 @@
+<script>
+import ConfigurationTable from './configuration_table.vue';
+
+export default {
+ components: {
+ ConfigurationTable,
+ },
+};
+</script>
+
+<template>
+ <article>
+ <header>
+ <h4 class="gl-my-5">
+ {{ __('Security Configuration') }}
+ </h4>
+ <h5 class="gl-font-lg gl-mt-7">
+ {{ s__('SecurityConfiguration|Testing & Compliance') }}
+ </h5>
+ </header>
+ <configuration-table />
+ </article>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/configuration_table.vue b/app/assets/javascripts/security_configuration/components/configuration_table.vue
new file mode 100644
index 00000000000..9475cc1781f
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/configuration_table.vue
@@ -0,0 +1,97 @@
+<script>
+import { GlLink, GlSprintf, GlTable, GlAlert } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+} from '~/vue_shared/security_reports/constants';
+import { features } from './features_constants';
+import ManageSast from './manage_sast.vue';
+import Upgrade from './upgrade.vue';
+
+const borderClasses = 'gl-border-b-1! gl-border-b-solid! gl-border-gray-100!';
+const thClass = `gl-text-gray-900 gl-bg-transparent! ${borderClasses}`;
+
+export default {
+ components: {
+ GlLink,
+ GlSprintf,
+ GlTable,
+ GlAlert,
+ },
+ data: () => ({
+ features,
+ errorMessage: '',
+ }),
+ methods: {
+ getFeatureDocumentationLinkLabel(item) {
+ return sprintf(s__('SecurityConfiguration|Feature documentation for %{featureName}'), {
+ featureName: item.name,
+ });
+ },
+ onError(value) {
+ this.errorMessage = value;
+ },
+ getComponentForItem(item) {
+ const COMPONENTS = {
+ [REPORT_TYPE_SAST]: ManageSast,
+ [REPORT_TYPE_DAST]: Upgrade,
+ [REPORT_TYPE_DEPENDENCY_SCANNING]: Upgrade,
+ [REPORT_TYPE_CONTAINER_SCANNING]: Upgrade,
+ [REPORT_TYPE_COVERAGE_FUZZING]: Upgrade,
+ [REPORT_TYPE_LICENSE_COMPLIANCE]: Upgrade,
+ };
+
+ return COMPONENTS[item.type];
+ },
+ },
+ table: {
+ fields: [
+ {
+ key: 'feature',
+ label: s__('SecurityConfiguration|Security Control'),
+ thClass,
+ },
+ {
+ key: 'manage',
+ label: s__('SecurityConfiguration|Manage'),
+ thClass,
+ },
+ ],
+ items: features,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert v-if="errorMessage" variant="danger" :dismissible="false">
+ {{ errorMessage }}
+ </gl-alert>
+ <gl-table :items="$options.table.items" :fields="$options.table.fields" stacked="md">
+ <template #cell(feature)="{ item }">
+ <div class="gl-text-gray-900">
+ {{ item.name }}
+ </div>
+ <div>
+ {{ item.description }}
+ <gl-link
+ target="_blank"
+ :href="item.link"
+ :aria-label="getFeatureDocumentationLinkLabel(item)"
+ >
+ {{ s__('SecurityConfiguration|More information') }}
+ </gl-link>
+ </div>
+ </template>
+
+ <template #cell(manage)="{ item }">
+ <component :is="getComponentForItem(item)" :data-testid="item.type" @error="onError" />
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/features_constants.js b/app/assets/javascripts/security_configuration/components/features_constants.js
new file mode 100644
index 00000000000..d846a2761d9
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/features_constants.js
@@ -0,0 +1,112 @@
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
+
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_SECRET_DETECTION,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+} from '~/vue_shared/security_reports/constants';
+
+/**
+ * Translations & helpPagePaths for Static Security Configuration Page
+ */
+export const SAST_NAME = s__('Static Application Security Testing (SAST)');
+export const SAST_DESCRIPTION = s__('Analyze your source code for known vulnerabilities.');
+export const SAST_HELP_PATH = helpPagePath('user/application_security/sast/index');
+
+export const DAST_NAME = s__('Dynamic Application Security Testing (DAST)');
+export const DAST_DESCRIPTION = s__('Analyze a review version of your web application.');
+export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index');
+
+export const SECRET_DETECTION_NAME = s__('Secret Detection');
+export const SECRET_DETECTION_DESCRIPTION = s__(
+ 'Analyze your source code and git history for secrets.',
+);
+export const SECRET_DETECTION_HELP_PATH = helpPagePath(
+ 'user/application_security/secret_detection/index',
+);
+
+export const DEPENDENCY_SCANNING_NAME = s__('Dependency Scanning');
+export const DEPENDENCY_SCANNING_DESCRIPTION = s__(
+ 'Analyze your dependencies for known vulnerabilities.',
+);
+export const DEPENDENCY_SCANNING_HELP_PATH = helpPagePath(
+ 'user/application_security/dependency_scanning/index',
+);
+
+export const CONTAINER_SCANNING_NAME = s__('Container Scanning');
+export const CONTAINER_SCANNING_DESCRIPTION = s__(
+ 'Check your Docker images for known vulnerabilities.',
+);
+export const CONTAINER_SCANNING_HELP_PATH = helpPagePath(
+ 'user/application_security/container_scanning/index',
+);
+
+export const COVERAGE_FUZZING_NAME = s__('Coverage Fuzzing');
+export const COVERAGE_FUZZING_DESCRIPTION = s__(
+ 'Find bugs in your code with coverage-guided fuzzing.',
+);
+export const COVERAGE_FUZZING_HELP_PATH = helpPagePath(
+ 'user/application_security/coverage_fuzzing/index',
+);
+
+export const LICENSE_COMPLIANCE_NAME = s__('License Compliance');
+export const LICENSE_COMPLIANCE_DESCRIPTION = s__(
+ 'Search your project dependencies for their licenses and apply policies.',
+);
+export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath(
+ 'user/compliance/license_compliance/index',
+);
+
+export const UPGRADE_CTA = s__(
+ 'SecurityConfiguration|Available with %{linkStart}upgrade or free trial%{linkEnd}',
+);
+
+export const features = [
+ {
+ name: SAST_NAME,
+ description: SAST_DESCRIPTION,
+ helpPath: SAST_HELP_PATH,
+ type: REPORT_TYPE_SAST,
+ },
+ {
+ name: DAST_NAME,
+ description: DAST_DESCRIPTION,
+ helpPath: DAST_HELP_PATH,
+ type: REPORT_TYPE_DAST,
+ },
+ {
+ name: SECRET_DETECTION_NAME,
+ description: SECRET_DETECTION_DESCRIPTION,
+ helpPath: SECRET_DETECTION_HELP_PATH,
+ type: REPORT_TYPE_SECRET_DETECTION,
+ },
+ {
+ name: DEPENDENCY_SCANNING_NAME,
+ description: DEPENDENCY_SCANNING_DESCRIPTION,
+ helpPath: DEPENDENCY_SCANNING_HELP_PATH,
+ type: REPORT_TYPE_DEPENDENCY_SCANNING,
+ },
+ {
+ name: CONTAINER_SCANNING_NAME,
+ description: CONTAINER_SCANNING_DESCRIPTION,
+ helpPath: CONTAINER_SCANNING_HELP_PATH,
+ type: REPORT_TYPE_CONTAINER_SCANNING,
+ },
+ {
+ name: COVERAGE_FUZZING_NAME,
+ description: COVERAGE_FUZZING_DESCRIPTION,
+ helpPath: COVERAGE_FUZZING_HELP_PATH,
+ type: REPORT_TYPE_COVERAGE_FUZZING,
+ },
+ {
+ name: LICENSE_COMPLIANCE_NAME,
+ description: LICENSE_COMPLIANCE_DESCRIPTION,
+ helpPath: LICENSE_COMPLIANCE_HELP_PATH,
+ type: REPORT_TYPE_LICENSE_COMPLIANCE,
+ },
+];
diff --git a/app/assets/javascripts/security_configuration/components/manage_sast.vue b/app/assets/javascripts/security_configuration/components/manage_sast.vue
new file mode 100644
index 00000000000..5169096d563
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/manage_sast.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
+import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
+
+export default {
+ components: {
+ GlButton,
+ },
+ inject: {
+ projectPath: {
+ from: 'projectPath',
+ default: '',
+ },
+ },
+ data: () => ({
+ isLoading: false,
+ }),
+ methods: {
+ async mutate() {
+ this.isLoading = true;
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: configureSastMutation,
+ variables: {
+ input: {
+ projectPath: this.projectPath,
+ configuration: { global: [], pipeline: [], analyzers: [] },
+ },
+ },
+ });
+ const { errors, successPath } = data.configureSast;
+
+ if (errors.length > 0) {
+ throw new Error(errors[0]);
+ }
+
+ if (!successPath) {
+ throw new Error(s__('SecurityConfiguration|SAST merge request creation mutation failed'));
+ }
+
+ redirectTo(successPath);
+ } catch (e) {
+ this.$emit('error', e.message);
+ this.isLoading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button :loading="isLoading" variant="success" category="secondary" @click="mutate">{{
+ s__('SecurityConfiguration|Configure via Merge Request')
+ }}</gl-button>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/upgrade.vue b/app/assets/javascripts/security_configuration/components/upgrade.vue
new file mode 100644
index 00000000000..166ee4ff194
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/upgrade.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { UPGRADE_CTA } from './features_constants';
+
+export default {
+ components: {
+ GlLink,
+ GlSprintf,
+ },
+ i18n: {
+ UPGRADE_CTA,
+ },
+};
+</script>
+
+<template>
+ <span>
+ <gl-sprintf :message="$options.i18n.UPGRADE_CTA">
+ <template #link="{ content }">
+ <gl-link target="_blank" href="https://about.gitlab.com/pricing/">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+</template>
diff --git a/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql
new file mode 100644
index 00000000000..9e826cf9e4b
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql
@@ -0,0 +1,6 @@
+mutation configureSast($input: ConfigureSastInput!) {
+ configureSast(input: $input) {
+ successPath
+ errors
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
new file mode 100644
index 00000000000..c98fa46b32b
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import SecurityConfigurationApp from './components/app.vue';
+
+export const initStaticSecurityConfiguration = (el) => {
+ if (!el) {
+ return null;
+ }
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { projectPath } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ projectPath,
+ },
+ render(createElement) {
+ return createElement(SecurityConfigurationApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
index 6776a9ebb22..9b7264e92ce 100644
--- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -1,10 +1,11 @@
<script>
/* eslint-disable vue/no-v-html */
-import Vue from 'vue';
import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle } from '@gitlab/ui';
+import Vue from 'vue';
import { mapState, mapActions } from 'vuex';
-import { __, s__, sprintf } from '~/locale';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
+import { __, s__, sprintf } from '~/locale';
Vue.use(GlToast);
@@ -98,11 +99,11 @@ export default {
'resetAlert',
]),
hideSelfMonitorModal() {
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
this.setSelfMonitor(true);
},
showSelfMonitorModal() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
saveChangesSelfMonitorProject() {
if (this.projectCreated && !this.projectEnabled) {
diff --git a/app/assets/javascripts/self_monitor/index.js b/app/assets/javascripts/self_monitor/index.js
index 7db87b4c627..384b73e528e 100644
--- a/app/assets/javascripts/self_monitor/index.js
+++ b/app/assets/javascripts/self_monitor/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from './store';
import SelfMonitorForm from './components/self_monitor_form.vue';
+import store from './store';
export default () => {
const el = document.querySelector('.js-self-monitoring-settings');
diff --git a/app/assets/javascripts/self_monitor/store/actions.js b/app/assets/javascripts/self_monitor/store/actions.js
index 99731309a62..670ee547b02 100644
--- a/app/assets/javascripts/self_monitor/store/actions.js
+++ b/app/assets/javascripts/self_monitor/store/actions.js
@@ -1,7 +1,7 @@
-import { __, s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
-import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
+import statusCodes from '~/lib/utils/http_status';
+import { __, s__ } from '~/locale';
import * as types from './mutation_types';
const TWO_MINUTES = 120000;
diff --git a/app/assets/javascripts/self_monitor/store/index.js b/app/assets/javascripts/self_monitor/store/index.js
index 1144fff759a..187c3bdd803 100644
--- a/app/assets/javascripts/self_monitor/store/index.js
+++ b/app/assets/javascripts/self_monitor/store/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
+import createState from './state';
Vue.use(Vuex);
diff --git a/app/assets/javascripts/sentry/sentry_config.js b/app/assets/javascripts/sentry/sentry_config.js
index 631d5448d1e..4277ffec545 100644
--- a/app/assets/javascripts/sentry/sentry_config.js
+++ b/app/assets/javascripts/sentry/sentry_config.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import * as Sentry from '~/sentry/wrapper';
import { __ } from '~/locale';
+import * as Sentry from '~/sentry/wrapper';
const IGNORE_ERRORS = [
// Random plugins/extensions
diff --git a/app/assets/javascripts/sentry_error_stack_trace/index.js b/app/assets/javascripts/sentry_error_stack_trace/index.js
index 80fa0988f0a..8e9ee25e7a8 100644
--- a/app/assets/javascripts/sentry_error_stack_trace/index.js
+++ b/app/assets/javascripts/sentry_error_stack_trace/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import SentryErrorStackTrace from './components/sentry_error_stack_trace.vue';
import store from '~/error_tracking/store';
+import SentryErrorStackTrace from './components/sentry_error_stack_trace.vue';
export default function initSentryErrorStacktrace() {
const sentryErrorStackTraceEl = document.querySelector('#js-sentry-error-stack-trace');
diff --git a/app/assets/javascripts/serverless/components/area.vue b/app/assets/javascripts/serverless/components/area.vue
index 056b342cf39..a9584c070fe 100644
--- a/app/assets/javascripts/serverless/components/area.vue
+++ b/app/assets/javascripts/serverless/components/area.vue
@@ -2,9 +2,9 @@
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
import { X_INTERVAL } from '../constants';
import { validateGraphData } from '../utils';
-import { __ } from '~/locale';
let debouncedResize;
diff --git a/app/assets/javascripts/serverless/components/environment_row.vue b/app/assets/javascripts/serverless/components/environment_row.vue
index c46dfb66afe..01030172ea8 100644
--- a/app/assets/javascripts/serverless/components/environment_row.vue
+++ b/app/assets/javascripts/serverless/components/environment_row.vue
@@ -1,6 +1,6 @@
<script>
-import FunctionRow from './function_row.vue';
import ItemCaret from '~/groups/components/item_caret.vue';
+import FunctionRow from './function_row.vue';
export default {
components: {
diff --git a/app/assets/javascripts/serverless/components/function_details.vue b/app/assets/javascripts/serverless/components/function_details.vue
index 6ab44eec5cd..d2306c2d8bd 100644
--- a/app/assets/javascripts/serverless/components/function_details.vue
+++ b/app/assets/javascripts/serverless/components/function_details.vue
@@ -1,10 +1,10 @@
<script>
import { isString } from 'lodash';
import { mapState, mapActions, mapGetters } from 'vuex';
-import PodBox from './pod_box.vue';
-import Url from './url.vue';
import AreaChart from './area.vue';
import MissingPrometheus from './missing_prometheus.vue';
+import PodBox from './pod_box.vue';
+import Url from './url.vue';
export default {
components: {
diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue
index aac7c46a295..fab9c0a75e7 100644
--- a/app/assets/javascripts/serverless/components/function_row.vue
+++ b/app/assets/javascripts/serverless/components/function_row.vue
@@ -1,8 +1,8 @@
<script>
import { isString } from 'lodash';
+import { visitUrl } from '~/lib/utils/url_utility';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import Url from './url.vue';
-import { visitUrl } from '~/lib/utils/url_utility';
export default {
components: {
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index d662cc7b802..b2d7aa75051 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -1,10 +1,10 @@
<script>
-import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLink, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { mapState, mapActions, mapGetters } from 'vuex';
import { sprintf, s__ } from '~/locale';
-import EnvironmentRow from './environment_row.vue';
-import EmptyState from './empty_state.vue';
import { CHECKING_INSTALLED } from '../constants';
+import EmptyState from './empty_state.vue';
+import EnvironmentRow from './environment_row.vue';
export default {
components: {
@@ -69,18 +69,16 @@ export default {
<div v-else-if="isInstalled">
<div v-if="hasFunctionData">
- <template>
- <div class="groups-list-tree-container js-functions-wrapper">
- <ul class="content-list group-list-tree">
- <environment-row
- v-for="(env, index) in getFunctions"
- :key="index"
- :env="env"
- :env-name="index"
- />
- </ul>
- </div>
- </template>
+ <div class="groups-list-tree-container js-functions-wrapper">
+ <ul class="content-list group-list-tree">
+ <environment-row
+ v-for="(env, index) in getFunctions"
+ :key="index"
+ :env="env"
+ :env-name="index"
+ />
+ </ul>
+ </div>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3 js-functions-loader" />
</div>
<div v-else class="empty-state js-empty-state">
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
index 24a9b4ac521..e8d87a40fc7 100644
--- a/app/assets/javascripts/serverless/serverless_bundle.js
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import Functions from './components/functions.vue';
import FunctionDetails from './components/function_details.vue';
+import Functions from './components/functions.vue';
import { createStore } from './store';
export default class Serverless {
diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js
index acd7020f70f..a6c0380a789 100644
--- a/app/assets/javascripts/serverless/store/actions.js
+++ b/app/assets/javascripts/serverless/store/actions.js
@@ -1,10 +1,10 @@
-import * as types from './mutation_types';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import statusCodes from '~/lib/utils/http_status';
import { __ } from '~/locale';
import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants';
+import * as types from './mutation_types';
export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING);
export const receiveFunctionsSuccess = ({ commit }, data) =>
diff --git a/app/assets/javascripts/serverless/survey_banner.vue b/app/assets/javascripts/serverless/survey_banner.vue
index 18ab8315840..c48c294c0f7 100644
--- a/app/assets/javascripts/serverless/survey_banner.vue
+++ b/app/assets/javascripts/serverless/survey_banner.vue
@@ -1,6 +1,6 @@
<script>
-import Cookies from 'js-cookie';
import { GlBanner } from '@gitlab/ui';
+import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
export default {
diff --git a/app/assets/javascripts/set_status_modal/components/user_availability_status.vue b/app/assets/javascripts/set_status_modal/components/user_availability_status.vue
deleted file mode 100644
index e86d94f86c6..00000000000
--- a/app/assets/javascripts/set_status_modal/components/user_availability_status.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-import { AVAILABILITY_STATUS, isUserBusy, isValidAvailibility } from '../utils';
-
-export default {
- name: 'UserAvailabilityStatus',
- props: {
- availability: {
- type: String,
- required: true,
- validator: isValidAvailibility,
- },
- },
- computed: {
- isBusy() {
- const { availability = AVAILABILITY_STATUS.NOT_SET } = this;
- return isUserBusy(availability);
- },
- },
-};
-</script>
-
-<template>
- <span v-if="isBusy" class="gl-font-weight-normal gl-text-gray-500">{{
- s__('UserAvailability|(Busy)')
- }}</span>
-</template>
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 c8efbd73b48..bed264341a5 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
@@ -1,15 +1,16 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlToast, GlModal, GlTooltipDirective, GlIcon, GlFormCheckbox } from '@gitlab/ui';
import $ from 'jquery';
import Vue from 'vue';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
-import { GlToast, GlModal, GlTooltipDirective, GlIcon, GlFormCheckbox } from '@gitlab/ui';
+import * as Emoji from '~/emoji';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { __, s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api';
import EmojiMenuInModal from './emoji_menu_in_modal';
-import { isUserBusy, isValidAvailibility } from './utils';
-import * as Emoji from '~/emoji';
+import { isUserBusy } from './utils';
const emojiMenuClass = 'js-modal-status-emoji-menu';
export const AVAILABILITY_STATUS = {
@@ -45,7 +46,6 @@ export default {
currentAvailability: {
type: String,
required: false,
- validator: isValidAvailibility,
default: '',
},
canSetUserAvailability: {
@@ -76,14 +76,14 @@ export default {
},
},
mounted() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
beforeDestroy() {
this.emojiMenu.destroy();
},
methods: {
closeModal() {
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
setupEmojiListAndAutocomplete() {
const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu';
diff --git a/app/assets/javascripts/set_status_modal/utils.js b/app/assets/javascripts/set_status_modal/utils.js
index faee4012ef4..e17d95adb25 100644
--- a/app/assets/javascripts/set_status_modal/utils.js
+++ b/app/assets/javascripts/set_status_modal/utils.js
@@ -3,7 +3,5 @@ export const AVAILABILITY_STATUS = {
NOT_SET: 'not_set',
};
-export const isUserBusy = (status) => status === AVAILABILITY_STATUS.BUSY;
-
-export const isValidAvailibility = (availability) =>
- availability.length ? Object.values(AVAILABILITY_STATUS).includes(availability) : true;
+export const isUserBusy = (status = '') =>
+ Boolean(status.length && status.toLowerCase().trim() === AVAILABILITY_STATUS.BUSY);
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index 1f1f6e42576..2c6da5669ef 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -1,7 +1,22 @@
import $ from 'jquery';
import { __ } from './locale';
-export function expandSection($section) {
+/**
+ * Returns true if the given section is expanded or not
+ *
+ * For legacy consistency, it supports both jQuery and DOM elements
+ *
+ * @param {jQuery | Element} section
+ */
+export function isExpanded(sectionArg) {
+ const section = sectionArg instanceof $ ? sectionArg[0] : sectionArg;
+
+ return section.classList.contains('expanded');
+}
+
+export function expandSection(sectionArg) {
+ const $section = $(sectionArg);
+
$section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Collapse'));
// eslint-disable-next-line @gitlab/no-global-event-off
$section.find('.settings-content').off('scroll.expandSection').scrollTop(0);
@@ -13,7 +28,9 @@ export function expandSection($section) {
}
}
-export function closeSection($section) {
+export function closeSection(sectionArg) {
+ const $section = $(sectionArg);
+
$section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Expand'));
$section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));
$section.removeClass('expanded');
@@ -26,7 +43,7 @@ export function closeSection($section) {
export function toggleSection($section) {
$section.removeClass('no-animate');
- if ($section.hasClass('expanded')) {
+ if (isExpanded($section)) {
closeSection($section);
} else {
expandSection($section);
@@ -38,7 +55,7 @@ export default function initSettingsPanels() {
const $section = $(elm);
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
- if (!$section.hasClass('expanded')) {
+ if (!isExpanded($section)) {
$section.find('.settings-content').on('scroll.expandSection', () => {
$section.removeClass('no-animate');
expandSection($section);
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
index 9ee02f923d5..467cd321fb8 100644
--- a/app/assets/javascripts/shared/milestones/form.js
+++ b/app/assets/javascripts/shared/milestones/form.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import ZenMode from '../../zen_mode';
import DueDateSelectors from '../../due_date_select';
import GLForm from '../../gl_form';
+import ZenMode from '../../zen_mode';
export default (initGFM = true) => {
new ZenMode(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
index fbbe2e341a7..d0a65b48522 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -1,8 +1,46 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
+import { isUserBusy } from '~/set_status_modal/utils';
import AssigneeAvatar from './assignee_avatar.vue';
+const I18N = {
+ BUSY: __('Busy'),
+ CANNOT_MERGE: __('Cannot merge'),
+ LC_CANNOT_MERGE: __('cannot merge'),
+};
+
+const paranthesize = (str) => `(${str})`;
+
+const generateAssigneeTooltip = ({
+ name,
+ availability,
+ cannotMerge = true,
+ tooltipHasName = false,
+}) => {
+ if (!tooltipHasName) {
+ return cannotMerge ? I18N.CANNOT_MERGE : '';
+ }
+
+ const statusInformation = [];
+ if (availability && isUserBusy(availability)) {
+ statusInformation.push(I18N.BUSY);
+ }
+
+ if (cannotMerge) {
+ statusInformation.push(I18N.LC_CANNOT_MERGE);
+ }
+
+ if (tooltipHasName && statusInformation.length) {
+ return sprintf(__('%{name} %{status}'), {
+ name,
+ status: statusInformation.map(paranthesize).join(' '),
+ });
+ }
+
+ return name;
+};
+
export default {
components: {
AssigneeAvatar,
@@ -37,15 +75,13 @@ export default {
return this.issuableType === 'merge_request' && !this.user.can_merge;
},
tooltipTitle() {
- if (this.cannotMerge && this.tooltipHasName) {
- return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name });
- } else if (this.cannotMerge) {
- return __('Cannot merge');
- } else if (this.tooltipHasName) {
- return this.user.name;
- }
-
- return '';
+ const { name = '', availability = '' } = this.user;
+ return generateAssigneeTooltip({
+ name,
+ availability,
+ cannotMerge: this.cannotMerge,
+ tooltipHasName: this.tooltipHasName,
+ });
},
tooltipOption() {
return {
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 84e7110e2b2..c3c009e680a 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -11,10 +11,6 @@ export default {
UncollapsedAssigneeList,
},
props: {
- rootPath: {
- type: String,
- required: true,
- },
users: {
type: Array,
required: true,
@@ -36,7 +32,6 @@ export default {
sortedAssigness() {
const canMergeUsers = this.users.filter((user) => user.can_merge);
const canNotMergeUsers = this.users.filter((user) => !user.can_merge);
-
return [...canMergeUsers, ...canNotMergeUsers];
},
},
@@ -52,9 +47,9 @@ export default {
<div>
<collapsed-assignee-list :users="sortedAssigness" :issuable-type="issuableType" />
- <div class="value hide-collapsed">
+ <div data-testid="expanded-assignee" class="value hide-collapsed">
<template v-if="hasNoUsers">
- <span class="assign-yourself no-value qa-assign-yourself">
+ <span class="assign-yourself no-value">
{{ __('None') }}
<template v-if="editable">
-
@@ -65,12 +60,7 @@ export default {
</span>
</template>
- <uncollapsed-assignee-list
- v-else
- :users="sortedAssigness"
- :root-path="rootPath"
- :issuable-type="issuableType"
- />
+ <uncollapsed-assignee-list v-else :users="sortedAssigness" :issuable-type="issuableType" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
index 0eee287e0c2..ca86d6c6c3e 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
@@ -1,7 +1,7 @@
<script>
+import actionCable from '~/actioncable_consumer';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
-import actionCable from '~/actioncable_consumer';
export default {
subscription: null,
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
index 2f654409561..af4227fa48d 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
@@ -1,9 +1,11 @@
<script>
import AssigneeAvatar from './assignee_avatar.vue';
+import UserNameWithStatus from './user_name_with_status.vue';
export default {
components: {
AssigneeAvatar,
+ UserNameWithStatus,
},
props: {
user: {
@@ -16,12 +18,20 @@ export default {
default: 'issue',
},
},
+ computed: {
+ availability() {
+ return this.user?.availability || '';
+ },
+ },
};
</script>
-
<template>
<button type="button" class="btn-link">
<assignee-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
- <span class="author"> {{ user.name }} </span>
+ <user-name-with-status
+ :name="user.name"
+ :availability="availability"
+ container-classes="author"
+ />
</button>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
index b713b0f960c..20667e695ce 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -1,11 +1,30 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
+import { isUserBusy } from '~/set_status_modal/utils';
import CollapsedAssignee from './collapsed_assignee.vue';
const DEFAULT_MAX_COUNTER = 99;
const DEFAULT_RENDER_COUNT = 5;
+const generateCollapsedAssigneeTooltip = ({ renderUsers, allUsers, tooltipTitleMergeStatus }) => {
+ const names = renderUsers.map(({ name, availability }) => {
+ if (availability && isUserBusy(availability)) {
+ return sprintf(__('%{name} (Busy)'), { name });
+ }
+ return name;
+ });
+
+ if (!allUsers.length) {
+ return __('Assignee(s)');
+ }
+ if (allUsers.length > names.length) {
+ names.push(sprintf(__('+ %{amount} more'), { amount: allUsers.length - names.length }));
+ }
+ const text = names.join(', ');
+ return tooltipTitleMergeStatus ? `${text} (${tooltipTitleMergeStatus})` : text;
+};
+
export default {
directives: {
GlTooltip: GlTooltipDirective,
@@ -74,19 +93,11 @@ export default {
tooltipTitle() {
const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length);
const renderUsers = this.users.slice(0, maxRender);
- const names = renderUsers.map((u) => u.name);
-
- if (!this.users.length) {
- return __('Assignee(s)');
- }
-
- if (this.users.length > names.length) {
- names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length }));
- }
-
- const text = names.join(', ');
-
- return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text;
+ return generateCollapsedAssigneeTooltip({
+ renderUsers,
+ allUsers: this.users,
+ tooltipTitleMergeStatus: this.tooltipTitleMergeStatus,
+ });
},
tooltipOptions() {
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
index 3c1b3afe889..e2dc37a0ac2 100644
--- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -8,12 +8,16 @@ export default {
GlButton,
UncollapsedAssigneeList,
},
- inject: ['rootPath'],
props: {
users: {
type: Array,
required: true,
},
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
},
computed: {
assigneesText() {
@@ -36,9 +40,9 @@ export default {
variant="link"
@click="$emit('assign-self')"
>
- <span class="gl-text-gray-400">{{ __('assign yourself') }}</span>
+ <span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
</gl-button>
</div>
- <uncollapsed-assignee-list v-else :users="users" :root-path="rootPath" />
+ <uncollapsed-assignee-list v-else :users="users" :issuable-type="issuableType" />
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index b9f268629fb..6595debf9a5 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,13 +1,13 @@
<script>
+import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
-import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
import AssigneesRealtime from './assignees_realtime.vue';
-import { __ } from '~/locale';
export default {
name: 'SidebarAssignees',
@@ -44,6 +44,11 @@ export default {
type: String,
required: true,
},
+ assigneeAvailabilityStatus: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
data() {
return {
@@ -101,6 +106,13 @@ export default {
return new Flash(__('Error occurred when saving assignees'));
});
},
+ exposeAvailabilityStatus(users) {
+ return users.map(({ username, ...rest }) => ({
+ ...rest,
+ username,
+ availability: this.assigneeAvailabilityStatus[username] || '',
+ }));
+ },
},
};
</script>
@@ -123,7 +135,7 @@ export default {
<assignees
v-if="!store.isFetching.assignees"
:root-path="relativeUrlRoot"
- :users="store.assignees"
+ :users="exposeAvailabilityStatus(store.assignees)"
:editable="store.editable"
:issuable-type="issuableType"
class="value"
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
new file mode 100644
index 00000000000..8f3f77cb5f0
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -0,0 +1,403 @@
+<script>
+import {
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { cloneDeep } from 'lodash';
+import Vue from 'vue';
+import createFlash from '~/flash';
+import searchUsers from '~/graphql_shared/queries/users_search.query.graphql';
+import { IssuableType } from '~/issue_show/constants';
+import { __, n__ } from '~/locale';
+import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { assigneesQueries } from '~/sidebar/constants';
+import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
+
+export const assigneesWidget = Vue.observable({
+ updateAssignees: null,
+});
+
+export default {
+ i18n: {
+ unassigned: __('Unassigned'),
+ assignee: __('Assignee'),
+ assignees: __('Assignees'),
+ assignTo: __('Assign to'),
+ },
+ assigneesQueries,
+ components: {
+ SidebarEditableItem,
+ IssuableAssignees,
+ MultiSelectDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ },
+ props: {
+ iid: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ initialAssignees: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: IssuableType.Issue,
+ validator(value) {
+ return [IssuableType.Issue, IssuableType.MergeRequest].includes(value);
+ },
+ },
+ multipleAssignees: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ issuable: {},
+ searchUsers: [],
+ selected: [],
+ isSettingAssignees: false,
+ isSearching: false,
+ };
+ },
+ apollo: {
+ issuable: {
+ query() {
+ return this.$options.assigneesQueries[this.issuableType].query;
+ },
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data.issuable || data.project?.issuable;
+ },
+ result({ data }) {
+ const issuable = data.issuable || data.project?.issuable;
+ if (issuable) {
+ this.selected = this.moveCurrentUserToStart(cloneDeep(issuable.assignees.nodes));
+ }
+ },
+ error() {
+ createFlash({ message: __('An error occurred while fetching participants.') });
+ },
+ },
+ searchUsers: {
+ query: searchUsers,
+ variables() {
+ return {
+ search: this.search,
+ };
+ },
+ update(data) {
+ return data.users?.nodes || [];
+ },
+ debounce: 250,
+ skip() {
+ return this.isSearchEmpty;
+ },
+ error() {
+ createFlash({ message: __('An error occurred while searching users.') });
+ this.isSearching = false;
+ },
+ result() {
+ this.isSearching = false;
+ },
+ },
+ },
+ computed: {
+ queryVariables() {
+ return {
+ iid: this.iid,
+ fullPath: this.fullPath,
+ };
+ },
+ assignees() {
+ const currentAssignees = this.$apollo.queries.issuable.loading
+ ? this.initialAssignees
+ : this.issuable?.assignees?.nodes;
+ return currentAssignees || [];
+ },
+ participants() {
+ const users =
+ this.isSearchEmpty || this.isSearching
+ ? this.issuable?.participants?.nodes
+ : this.searchUsers;
+ return this.moveCurrentUserToStart(users);
+ },
+ assigneeText() {
+ const items = this.$apollo.queries.issuable.loading ? this.initialAssignees : this.selected;
+ return n__('Assignee', '%d Assignees', items.length);
+ },
+ selectedFiltered() {
+ if (this.isSearchEmpty || this.isSearching) {
+ return this.selected;
+ }
+
+ const foundUsernames = this.searchUsers.map(({ username }) => username);
+ return this.selected.filter(({ username }) => foundUsernames.includes(username));
+ },
+ unselectedFiltered() {
+ return (
+ this.participants?.filter(({ username }) => !this.selectedUserNames.includes(username)) ||
+ []
+ );
+ },
+ selectedIsEmpty() {
+ return this.selectedFiltered.length === 0;
+ },
+ selectedUserNames() {
+ return this.selected.map(({ username }) => username);
+ },
+ isSearchEmpty() {
+ return this.search === '';
+ },
+ currentUser() {
+ return {
+ username: gon?.current_username,
+ name: gon?.current_user_fullname,
+ avatarUrl: gon?.current_user_avatar_url,
+ };
+ },
+ isAssigneesLoading() {
+ return !this.initialAssignees && this.$apollo.queries.issuable.loading;
+ },
+ isCurrentUserInParticipants() {
+ const isCurrentUser = (user) => user.username === this.currentUser.username;
+ return this.selected.some(isCurrentUser) || this.participants.some(isCurrentUser);
+ },
+ noUsersFound() {
+ return !this.isSearchEmpty && this.unselectedFiltered.length === 0;
+ },
+ showCurrentUser() {
+ return !this.isCurrentUserInParticipants && (this.isSearchEmpty || this.isSearching);
+ },
+ },
+ watch: {
+ // We need to add this watcher to track the moment when user is alredy typing
+ // but query is still not started due to debounce
+ search(newVal) {
+ if (newVal) {
+ this.isSearching = true;
+ }
+ },
+ },
+ created() {
+ assigneesWidget.updateAssignees = this.updateAssignees;
+ },
+ destroyed() {
+ assigneesWidget.updateAssignees = null;
+ },
+ methods: {
+ updateAssignees(assigneeUsernames) {
+ this.isSettingAssignees = true;
+ return this.$apollo
+ .mutate({
+ mutation: this.$options.assigneesQueries[this.issuableType].mutation,
+ variables: {
+ ...this.queryVariables,
+ assigneeUsernames,
+ },
+ })
+ .then(({ data }) => {
+ this.$emit('assignees-updated', data);
+ return data;
+ })
+ .catch(() => {
+ createFlash({ message: __('An error occurred while updating assignees.') });
+ })
+ .finally(() => {
+ this.isSettingAssignees = false;
+ });
+ },
+ selectAssignee(name) {
+ if (name === undefined) {
+ this.clearSelected();
+ return;
+ }
+
+ if (!this.multipleAssignees) {
+ this.selected = [name];
+ this.collapseWidget();
+ } else {
+ this.selected = this.selected.concat(name);
+ }
+ },
+ unselect(name) {
+ this.selected = this.selected.filter((user) => user.username !== name);
+
+ if (!this.multipleAssignees) {
+ this.collapseWidget();
+ }
+ },
+ assignSelf() {
+ this.updateAssignees(this.currentUser.username);
+ },
+ clearSelected() {
+ this.selected = [];
+ },
+ saveAssignees() {
+ this.updateAssignees(this.selectedUserNames);
+ },
+ isChecked(id) {
+ return this.selectedUserNames.includes(id);
+ },
+ async focusSearch() {
+ await this.$nextTick();
+ this.$refs.search.focusInput();
+ },
+ moveCurrentUserToStart(users) {
+ if (!users) {
+ return [];
+ }
+ const usersCopy = [...users];
+ const currentUser = usersCopy.find((user) => user.username === this.currentUser.username);
+
+ if (currentUser) {
+ const index = usersCopy.indexOf(currentUser);
+ usersCopy.splice(0, 0, usersCopy.splice(index, 1)[0]);
+ }
+
+ return usersCopy;
+ },
+ collapseWidget() {
+ this.$refs.toggle.collapse();
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="isAssigneesLoading"
+ class="gl-display-flex gl-align-items-center assignee"
+ data-testid="loading-assignees"
+ >
+ {{ __('Assignee') }}
+ <gl-loading-icon size="sm" class="gl-ml-2" />
+ </div>
+ <sidebar-editable-item
+ v-else
+ ref="toggle"
+ :loading="isSettingAssignees"
+ :title="assigneeText"
+ @open="focusSearch"
+ @close="saveAssignees"
+ >
+ <template #collapsed>
+ <issuable-assignees
+ :users="assignees"
+ :issuable-type="issuableType"
+ @assign-self="assignSelf"
+ />
+ </template>
+
+ <template #default>
+ <multi-select-dropdown
+ class="gl-w-full dropdown-menu-user"
+ :text="$options.i18n.assignees"
+ :header-text="$options.i18n.assignTo"
+ @toggle="collapseWidget"
+ >
+ <template #search>
+ <gl-search-box-by-type ref="search" v-model.trim="search" />
+ </template>
+ <template #items>
+ <gl-loading-icon
+ v-if="$apollo.queries.searchUsers.loading || $apollo.queries.issuable.loading"
+ data-testid="loading-participants"
+ size="lg"
+ />
+ <template v-else>
+ <template v-if="isSearchEmpty || isSearching">
+ <gl-dropdown-item
+ :is-checked="selectedIsEmpty"
+ :is-check-centered="true"
+ data-testid="unassign"
+ @click="selectAssignee()"
+ >
+ <span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'">{{
+ $options.i18n.unassigned
+ }}</span></gl-dropdown-item
+ >
+ <gl-dropdown-divider data-testid="unassign-divider" />
+ </template>
+ <gl-dropdown-item
+ v-for="item in selectedFiltered"
+ :key="item.id"
+ :is-checked="isChecked(item.username)"
+ :is-check-centered="true"
+ data-testid="selected-participant"
+ @click.stop="unselect(item.username)"
+ >
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="item.name"
+ :sub-label="item.username"
+ :src="item.avatarUrl || item.avatar || item.avatar_url"
+ class="gl-align-items-center"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ <gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" />
+ <template v-if="showCurrentUser">
+ <gl-dropdown-item
+ data-testid="unselected-participant"
+ @click.stop="selectAssignee(currentUser)"
+ >
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="currentUser.name"
+ :sub-label="currentUser.username"
+ :src="currentUser.avatarUrl"
+ class="gl-align-items-center"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ </template>
+ <gl-dropdown-item
+ v-for="unselectedUser in unselectedFiltered"
+ :key="unselectedUser.id"
+ data-testid="unselected-participant"
+ @click="selectAssignee(unselectedUser)"
+ >
+ <gl-avatar-link class="gl-pl-6!">
+ <gl-avatar-labeled
+ :size="32"
+ :label="unselectedUser.name"
+ :sub-label="unselectedUser.username"
+ :src="unselectedUser.avatarUrl || unselectedUser.avatar"
+ class="gl-align-items-center"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="noUsersFound && !isSearching">
+ {{ __('No matching results') }}
+ </gl-dropdown-item>
+ </template>
+ </template>
+ </multi-select-dropdown>
+ </template>
+ </sidebar-editable-item>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index 31d5d7c0077..36775648809 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -1,12 +1,14 @@
<script>
import { __, sprintf } from '~/locale';
import AssigneeAvatarLink from './assignee_avatar_link.vue';
+import UserNameWithStatus from './user_name_with_status.vue';
const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
AssigneeAvatarLink,
+ UserNameWithStatus,
},
props: {
users: {
@@ -55,6 +57,9 @@ export default {
toggleShowLess() {
this.showLess = !this.showLess;
},
+ userAvailability(u) {
+ return u?.availability || '';
+ },
},
};
</script>
@@ -68,7 +73,7 @@ export default {
:issuable-type="issuableType"
>
<div class="ml-2 gl-line-height-normal">
- <div>{{ firstUser.name }}</div>
+ <user-name-with-status :name="firstUser.name" :availability="userAvailability(firstUser)" />
<div>{{ username }}</div>
</div>
</assignee-avatar-link>
diff --git a/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue b/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue
new file mode 100644
index 00000000000..41b3b6c9a45
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue
@@ -0,0 +1,40 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { isUserBusy } from '~/set_status_modal/utils';
+
+export default {
+ name: 'UserNameWithStatus',
+ components: {
+ GlSprintf,
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ containerClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ availability: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ isBusy() {
+ return isUserBusy(this.availability);
+ },
+ },
+};
+</script>
+<template>
+ <span :class="containerClasses">
+ <gl-sprintf v-if="isBusy" :message="s__('UserAvailability|%{author} (Busy)')">
+ <template #author>{{ name }}</template>
+ </gl-sprintf>
+ <template v-else>{{ name }}</template>
+ </span>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index ce120ff82f3..57b3705e803 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { mapState } from 'vuex';
import { __, sprintf } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import EditForm from './edit_form.vue';
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index 17e44cf0e1d..057224d5918 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf } from '@gitlab/ui';
-import editFormButtons from './edit_form_buttons.vue';
import { __ } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
export default {
components: {
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index d210f9efcb3..154a228c978 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -1,9 +1,9 @@
<script>
-import $ from 'jquery';
import { GlButton } from '@gitlab/ui';
+import $ from 'jquery';
import { mapActions } from 'vuex';
-import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import { __ } from '~/locale';
import eventHub from '../../event_hub';
export default {
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
index e01e1f032e3..c9b6616e067 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -3,13 +3,13 @@ import $ from 'jquery';
import { camelCase, difference, union } from 'lodash';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import createFlash from '~/flash';
+import { getIdFromGraphQLId, MutationOperationMode } from '~/graphql_shared/utils';
import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
import { toLabelGid } from '~/sidebar/utils';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
-import { getIdFromGraphQLId, MutationOperationMode } from '~/graphql_shared/utils';
const mutationMap = {
[IssuableType.Issue]: {
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index 26a7c8e4a80..c3f31a3d220 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,9 +1,9 @@
<script>
-import $ from 'jquery';
import { GlButton } from '@gitlab/ui';
+import $ from 'jquery';
import { mapActions } from 'vuex';
-import { __, sprintf } from '../../../locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import { __, sprintf } from '../../../locale';
import eventHub from '../../event_hub';
export default {
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index b96a2b93712..3468acb38e7 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
index b1b04564a62..87780888c2f 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue
@@ -76,8 +76,8 @@ export default {
class="d-inline-block"
>
<!-- use d-flex so that slot can be appropriately styled -->
- <span class="d-flex">
- <reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
+ <span class="gl-display-flex gl-align-items-center">
+ <reviewer-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
<slot :user="user"></slot>
</span>
</gl-link>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index cd62fe5be0f..2c52d7142f7 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -46,6 +46,9 @@ export default {
assignSelf() {
this.$emit('assign-self');
},
+ requestReview(data) {
+ this.$emit('request-review', data);
+ },
},
};
</script>
@@ -56,7 +59,7 @@ export default {
<div class="value hide-collapsed">
<template v-if="hasNoUsers">
- <span class="assign-yourself no-value qa-assign-yourself">
+ <span class="assign-yourself no-value">
{{ __('None') }}
</span>
</template>
@@ -66,6 +69,7 @@ export default {
:users="sortedReviewers"
:root-path="rootPath"
:issuable-type="issuableType"
+ @request-review="requestReview"
/>
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index 1a2473e5f6c..b5cf5df4957 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -1,14 +1,14 @@
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
-import { deprecatedCreateFlash as Flash } from '~/flash';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReviewerTitle from './reviewer_title.vue';
import Reviewers from './reviewers.vue';
-import { __ } from '~/locale';
export default {
name: 'SidebarReviewers',
@@ -83,6 +83,9 @@ export default {
return new Flash(__('Error occurred when saving reviewers'));
});
},
+ requestReview(data) {
+ this.mediator.requestReview(data);
+ },
},
};
</script>
@@ -101,6 +104,7 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
class="value"
+ @request-review="requestReview"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
index e82a271d007..cbd68f2513a 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
@@ -1,15 +1,19 @@
<script>
-// NOTE! For the first iteration, we are simply copying the implementation of Assignees
-// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
-import { __, sprintf } from '~/locale';
+import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
-const DEFAULT_RENDER_COUNT = 5;
+const LOADING_STATE = 'loading';
+const SUCCESS_STATE = 'success';
export default {
components: {
+ GlButton,
+ GlIcon,
ReviewerAvatarLink,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
users: {
type: Array,
@@ -28,76 +32,78 @@ export default {
data() {
return {
showLess: true,
+ loadingStates: {},
};
},
- computed: {
- firstUser() {
- return this.users[0];
- },
- hasOneUser() {
- return this.users.length === 1;
- },
- hiddenReviewersLabel() {
- const { numberOfHiddenReviewers } = this;
- return sprintf(__('+ %{numberOfHiddenReviewers} more'), { numberOfHiddenReviewers });
- },
- renderShowMoreSection() {
- return this.users.length > DEFAULT_RENDER_COUNT;
- },
- numberOfHiddenReviewers() {
- return this.users.length - DEFAULT_RENDER_COUNT;
- },
- uncollapsedUsers() {
- const uncollapsedLength = this.showLess
- ? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
- : this.users.length;
- return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
- },
- username() {
- return `@${this.firstUser.username}`;
+ watch: {
+ users: {
+ handler(users) {
+ this.loadingStates = users.reduce(
+ (acc, user) => ({
+ ...acc,
+ [user.id]: acc[user.id] || null,
+ }),
+ this.loadingStates,
+ );
+ },
+ immediate: true,
},
},
methods: {
toggleShowLess() {
this.showLess = !this.showLess;
},
+ reRequestReview(userId) {
+ this.loadingStates[userId] = LOADING_STATE;
+ this.$emit('request-review', { userId, callback: this.requestReviewComplete });
+ },
+ requestReviewComplete(userId, success) {
+ if (success) {
+ this.loadingStates[userId] = SUCCESS_STATE;
+
+ setTimeout(() => {
+ this.loadingStates[userId] = null;
+ }, 1500);
+ } else {
+ this.loadingStates[userId] = null;
+ }
+ },
},
+ LOADING_STATE,
+ SUCCESS_STATE,
};
</script>
<template>
- <reviewer-avatar-link
- v-if="hasOneUser"
- #default="{ user }"
- tooltip-placement="left"
- :tooltip-has-name="false"
- :user="firstUser"
- :root-path="rootPath"
- :issuable-type="issuableType"
- >
- <div class="gl-ml-3 gl-line-height-normal">
- <div class="author">{{ user.name }}</div>
- <div class="username">{{ username }}</div>
- </div>
- </reviewer-avatar-link>
- <div v-else>
- <div class="user-list">
- <div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
- <reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
- </div>
- </div>
- <div v-if="renderShowMoreSection" class="user-list-more">
- <button
- type="button"
- class="btn-link"
- data-qa-selector="more_reviewers_link"
- @click="toggleShowLess"
- >
- <template v-if="showLess">
- {{ hiddenReviewersLabel }}
- </template>
- <template v-else>{{ __('- show less') }}</template>
- </button>
+ <div>
+ <div
+ v-for="(user, index) in users"
+ :key="user.id"
+ :class="{ 'gl-mb-3': index !== users.length - 1 }"
+ data-testid="reviewer"
+ >
+ <reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType">
+ <div class="gl-ml-3">@{{ user.username }}</div>
+ </reviewer-avatar-link>
+ <gl-icon
+ v-if="loadingStates[user.id] === $options.SUCCESS_STATE"
+ :size="24"
+ name="check"
+ class="float-right gl-text-green-500"
+ data-testid="re-request-success"
+ />
+ <gl-button
+ v-else-if="user.can_update_merge_request && user.reviewed"
+ v-gl-tooltip.left
+ :title="__('Re-request review')"
+ :loading="loadingStates[user.id] === $options.LOADING_STATE"
+ class="float-right gl-text-gray-500!"
+ size="small"
+ icon="redo"
+ variant="link"
+ data-testid="re-request-button"
+ @click="reRequestReview(user.id)"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
index 0cf11e83349..6a6300dcde0 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -7,10 +7,10 @@ import {
GlSprintf,
GlLink,
} from '@gitlab/ui';
+import createFlash from '~/flash';
import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
import updateIssuableSeverity from './graphql/mutations/update_issuable_severity.mutation.graphql';
import SeverityToken from './severity.vue';
-import createFlash from '~/flash';
export default {
i18n: I18N,
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
new file mode 100644
index 00000000000..9da839cd133
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+
+export default {
+ components: { GlButton, GlLoadingIcon },
+ inject: ['canUpdate'],
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ edit: false,
+ };
+ },
+ destroyed() {
+ window.removeEventListener('click', this.collapseWhenOffClick);
+ window.removeEventListener('keyup', this.collapseOnEscape);
+ },
+ methods: {
+ collapseWhenOffClick({ target }) {
+ if (!this.$el.contains(target)) {
+ this.collapse();
+ }
+ },
+ collapseOnEscape({ key }) {
+ if (key === 'Escape') {
+ this.collapse();
+ }
+ },
+ expand() {
+ if (this.edit) {
+ return;
+ }
+
+ this.edit = true;
+ this.$emit('open');
+ window.addEventListener('click', this.collapseWhenOffClick);
+ window.addEventListener('keyup', this.collapseOnEscape);
+ },
+ collapse({ emitEvent = true } = {}) {
+ if (!this.edit) {
+ return;
+ }
+
+ this.edit = false;
+ if (emitEvent) {
+ this.$emit('close');
+ }
+ window.removeEventListener('click', this.collapseWhenOffClick);
+ window.removeEventListener('keyup', this.collapseOnEscape);
+ },
+ toggle({ emitEvent = true } = {}) {
+ if (this.edit) {
+ this.collapse({ emitEvent });
+ } else {
+ this.expand();
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-display-flex gl-align-items-center gl-mb-3" @click.self="collapse">
+ <span data-testid="title">{{ title }}</span>
+ <gl-loading-icon v-if="loading" inline class="gl-ml-2" />
+ <gl-button
+ v-if="canUpdate"
+ variant="link"
+ class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto js-sidebar-dropdown-toggle"
+ data-testid="edit-button"
+ @keyup.esc="toggle"
+ @click="toggle"
+ >
+ {{ __('Edit') }}
+ </gl-button>
+ </div>
+ <div v-show="!edit" class="gl-text-gray-500" data-testid="collapsed-content">
+ <slot name="collapsed">{{ __('None') }}</slot>
+ </div>
+ <div v-show="edit" data-testid="expanded-content">
+ <slot :edit="edit"></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index ee1c98e9d69..3ad097138a3 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -1,7 +1,7 @@
<script>
-import Store from '../../stores/sidebar_store';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __ } from '../../../locale';
+import Store from '../../stores/sidebar_store';
import subscriptions from './subscriptions.vue';
export default {
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 6d21936791c..9b06c20a6f3 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -1,8 +1,7 @@
<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import Tracking from '~/tracking';
-import toggleButton from '~/vue_shared/components/toggle_button.vue';
import eventHub from '../../event_hub';
const ICON_ON = 'notifications';
@@ -16,7 +15,7 @@ export default {
},
components: {
GlIcon,
- toggleButton,
+ GlToggle,
},
mixins: [Tracking.mixin({ label: 'right_sidebar' })],
props: {
@@ -106,7 +105,7 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-flex gl-justify-content-space-between">
<span
ref="tooltip"
v-gl-tooltip.viewport.left
@@ -116,13 +115,13 @@ export default {
>
<gl-icon :name="notificationIcon" :size="16" class="sidebar-item-icon is-active" />
</span>
- <span class="issuable-header-text hide-collapsed float-left"> {{ notificationText }} </span>
- <toggle-button
+ <span class="hide-collapsed" data-testid="subscription-title"> {{ notificationText }} </span>
+ <gl-toggle
v-if="!projectEmailsDisabled"
- ref="toggleButton"
:is-loading="showLoadingState"
:value="subscribed"
- class="float-right hide-collapsed js-issuable-subscribe-button"
+ class="hide-collapsed"
+ data-testid="subscription-toggle"
@change="toggleSubscription"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 8bc828091c0..e0f60b9af08 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
-import { sprintf, s__ } from '../../../locale';
import { joinPaths } from '~/lib/utils/url_utility';
+import { sprintf, s__ } from '../../../locale';
export default {
name: 'TimeTrackingHelpState',
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 26e0a0da860..c70d99ac178 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -4,11 +4,10 @@ import { intersection } from 'lodash';
import '~/smart_interval';
-import IssuableTimeTracker from './time_tracker.vue';
-
-import Store from '../../stores/sidebar_store';
-import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
+import Mediator from '../../sidebar_mediator';
+import Store from '../../stores/sidebar_store';
+import IssuableTimeTracker from './time_tracker.vue';
export default {
components: {
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 26b8e087512..4c095006dd7 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,12 +1,11 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { s__, __ } from '~/locale';
-import TimeTrackingHelpState from './help_state.vue';
+import eventHub from '../../event_hub';
import TimeTrackingCollapsedState from './collapsed_state.vue';
-import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
-
-import eventHub from '../../event_hub';
+import TimeTrackingHelpState from './help_state.vue';
+import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
export default {
name: 'IssuableTimeTracker',
@@ -48,11 +47,11 @@ export default {
/*
In issue list, "time-tracking-collapsed-state" is always rendered even if the sidebar isn't collapsed.
The actual hiding is controlled with css classes:
- Hide "time-tracking-collapsed-state"
+ Hide "time-tracking-collapsed-state"
if .right-sidebar .right-sidebar-collapsed .sidebar-collapsed-icon
Show "time-tracking-collapsed-state"
if .right-sidebar .right-sidebar-expanded .sidebar-collapsed-icon
-
+
In Swimlanes sidebar, we do not use collapsed state at all.
*/
showCollapsed: {
@@ -99,10 +98,12 @@ export default {
update(data) {
const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = data;
+ /* eslint-disable vue/no-mutating-props */
this.timeEstimate = timeEstimate;
this.timeSpent = timeSpent;
this.humanTimeEstimate = humanTimeEstimate;
this.humanTimeSpent = humanTimeSpent;
+ /* eslint-enable vue/no-mutating-props */
},
},
};
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index 1e3e870ec83..f589e7555b3 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -3,7 +3,7 @@ import { GlLoadingIcon, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
const MARK_TEXT = __('Mark as done');
-const TODO_TEXT = __('Add a To-Do');
+const TODO_TEXT = __('Add a to do');
export default {
components: {
@@ -42,7 +42,7 @@ export default {
buttonClasses() {
return this.collapsed
? 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state'
- : 'btn btn-default btn-todo issuable-header-btn float-right';
+ : 'gl-button btn btn-default btn-todo issuable-header-btn float-right';
},
buttonLabel() {
return this.isTodo ? MARK_TEXT : TODO_TEXT;
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
new file mode 100644
index 00000000000..274aa237aea
--- /dev/null
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -0,0 +1,16 @@
+import { IssuableType } from '~/issue_show/constants';
+import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
+import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql';
+import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
+import updateMergeRequestParticipantsMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
+
+export const assigneesQueries = {
+ [IssuableType.Issue]: {
+ query: getIssueParticipants,
+ mutation: updateAssigneesMutation,
+ },
+ [IssuableType.MergeRequest]: {
+ query: getMergeRequestParticipants,
+ mutation: updateMergeRequestParticipantsMutation,
+ },
+};
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 55847fc43f0..21cd24b0842 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { escape } from 'lodash';
-import { __ } from '~/locale';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __ } from '~/locale';
function isValidProjectId(id) {
return id > 0;
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index 4d9e99941d1..b11c8f76a6d 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import timeTracker from './components/time_tracking/time_tracker.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import timeTracker from './components/time_tracking/time_tracker.vue';
export default class SidebarMilestone {
constructor() {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 2760bf431ea..662edbc4f8d 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,22 +1,27 @@
import $ from 'jquery';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
+import createFlash from '~/flash';
+import createDefaultClient from '~/lib/graphql';
+import {
+ isInIssuePage,
+ isInDesignPage,
+ isInIncidentPage,
+ parseBoolean,
+} from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import Translate from '../vue_shared/translate';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
-import SidebarLabels from './components/labels/sidebar_labels.vue';
-import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
-import SidebarMoveIssue from './lib/sidebar_move_issue';
+import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
+import SidebarLabels from './components/labels/sidebar_labels.vue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
-import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
+import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
-import Translate from '../vue_shared/translate';
-import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
-import createDefaultClient from '~/lib/graphql';
-import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
-import { __ } from '~/locale';
+import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
+import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
+import SidebarMoveIssue from './lib/sidebar_move_issue';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -25,6 +30,28 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op
return JSON.parse(sidebarOptEl.innerHTML);
}
+/**
+ * Extracts the list of assignees with availability information from a hidden input
+ * field and converts to a key:value pair for use in the sidebar assignees component.
+ * The assignee username is used as the key and their busy status is the value
+ *
+ * e.g { root: 'busy', admin: '' }
+ *
+ * @returns {Object}
+ */
+function getSidebarAssigneeAvailabilityData() {
+ const sidebarAssigneeEl = document.querySelectorAll('.js-sidebar-assignee-data input');
+ return Array.from(sidebarAssigneeEl)
+ .map((el) => el.dataset)
+ .reduce(
+ (acc, { username, availability = '' }) => ({
+ ...acc,
+ [username]: availability,
+ }),
+ {},
+ );
+}
+
function mountAssigneesComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-assignees');
const apolloProvider = new VueApollo({
@@ -34,6 +61,7 @@ function mountAssigneesComponent(mediator) {
if (!el) return;
const { iid, fullPath } = getSidebarOptions();
+ const assigneeAvailabilityStatus = getSidebarAssigneeAvailabilityData();
// eslint-disable-next-line no-new
new Vue({
el,
@@ -49,7 +77,9 @@ function mountAssigneesComponent(mediator) {
projectPath: fullPath,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
- issuableType: isInIssuePage() || isInIncidentPage() ? 'issue' : 'merge_request',
+ issuableType:
+ isInIssuePage() || isInIncidentPage() || isInDesignPage() ? 'issue' : 'merge_request',
+ assigneeAvailabilityStatus,
},
}),
});
@@ -78,7 +108,7 @@ function mountReviewersComponent(mediator) {
issuableIid: String(iid),
projectPath: fullPath,
field: el.dataset.field,
- issuableType: isInIssuePage() ? 'issue' : 'merge_request',
+ issuableType: isInIssuePage() || isInDesignPage() ? 'issue' : 'merge_request',
},
}),
});
diff --git a/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql b/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql
new file mode 100644
index 00000000000..73765e7d77b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql
@@ -0,0 +1,5 @@
+mutation mergeRequestRequestRereview($projectPath: ID!, $iid: String!, $userId: ID!) {
+ mergeRequestReviewerRereview(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index a61af631661..f31e4a3e0dd 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -1,6 +1,8 @@
import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
-import axios from '~/lib/utils/axios_utils';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
+import axios from '~/lib/utils/axios_utils';
+import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
export const gqClient = createGqClient(
{},
@@ -70,4 +72,15 @@ export default class SidebarService {
move_to_project_id: moveToProjectId,
});
}
+
+ requestReview(userId) {
+ return gqClient.mutate({
+ mutation: reviewerRereviewMutation,
+ variables: {
+ userId: convertToGraphQLId('User', `${userId}`), // eslint-disable-line @gitlab/require-i18n-strings
+ projectPath: this.fullPath,
+ iid: this.iid.toString(),
+ },
+ });
+ }
}
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 377846db70e..063e3313a3c 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -1,5 +1,5 @@
-import Mediator from './sidebar_mediator';
import { mountSidebar, getSidebarOptions } from './mount_sidebar';
+import Mediator from './sidebar_mediator';
export default () => {
const mediator = new Mediator(getSidebarOptions());
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index d143283653b..bd382ed0fdb 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,8 +1,9 @@
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
-import { visitUrl } from '../lib/utils/url_utility';
+import { __ } from '~/locale';
+import toast from '~/vue_shared/plugins/global_toast';
import { deprecatedCreateFlash as Flash } from '../flash';
+import { visitUrl } from '../lib/utils/url_utility';
import Service from './services/sidebar_service';
-import { __ } from '~/locale';
export default class SidebarMediator {
constructor(options) {
@@ -51,6 +52,17 @@ export default class SidebarMediator {
return this.service.update(field, data);
}
+ requestReview({ userId, callback }) {
+ return this.service
+ .requestReview(userId)
+ .then(() => {
+ this.store.updateReviewer(userId);
+ toast(__('Requested review'));
+ callback(userId, true);
+ })
+ .catch(() => callback(userId, false));
+ }
+
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index d53393052eb..3c108b06eab 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -96,6 +96,14 @@ export default class SidebarStore {
}
}
+ updateReviewer(id) {
+ const reviewer = this.findReviewer({ id });
+
+ if (reviewer) {
+ reviewer.reviewed = false;
+ }
+ }
+
findAssignee(findAssignee) {
return this.assignees.find(({ id }) => id === findAssignee.id);
}
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 192eb0784d4..687289b6675 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,13 +1,13 @@
/* eslint-disable consistent-return */
import $ from 'jquery';
-import { __ } from './locale';
-import axios from './lib/utils/axios_utils';
-import { deprecatedCreateFlash as createFlash } from './flash';
+import { spriteIcon } from '~/lib/utils/common_utils';
import FilesCommentButton from './files_comment_button';
+import { deprecatedCreateFlash as createFlash } from './flash';
import initImageDiffHelper from './image_diff/helpers/init_image_diff';
+import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
import syntaxHighlight from './syntax_highlight';
-import { spriteIcon } from '~/lib/utils/common_utils';
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<span class="spinner"></span>';
diff --git a/app/assets/javascripts/snippet/snippet_edit.js b/app/assets/javascripts/snippet/snippet_edit.js
index 88677ddd15f..4fbd0b987fe 100644
--- a/app/assets/javascripts/snippet/snippet_edit.js
+++ b/app/assets/javascripts/snippet/snippet_edit.js
@@ -1,6 +1,6 @@
-import ZenMode from '~/zen_mode';
-import SnippetsEdit from '~/snippets/components/edit.vue';
import SnippetsAppFactory from '~/snippets';
+import SnippetsEdit from '~/snippets/components/edit.vue';
+import ZenMode from '~/zen_mode';
SnippetsAppFactory(document.getElementById('js-snippet-edit'), SnippetsEdit);
new ZenMode(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/snippet/snippet_show.js b/app/assets/javascripts/snippet/snippet_show.js
index caa76fc9988..22dffa90cef 100644
--- a/app/assets/javascripts/snippet/snippet_show.js
+++ b/app/assets/javascripts/snippet/snippet_show.js
@@ -1,7 +1,7 @@
-import initNotes from '~/init_notes';
import loadAwardsHandler from '~/awards_handler';
-import SnippetsShow from '~/snippets/components/show.vue';
+import initNotes from '~/init_notes';
import SnippetsAppFactory from '~/snippets';
+import SnippetsShow from '~/snippets/components/show.vue';
import ZenMode from '~/zen_mode';
SnippetsAppFactory(document.getElementById('js-snippet-view'), SnippetsShow);
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index ffb5e242973..9f43ac36df7 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -1,27 +1,28 @@
<script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import eventHub from '~/blob/components/eventhub';
import { deprecatedCreateFlash as Flash } from '~/flash';
-import { __, sprintf } from '~/locale';
-import TitleField from '~/vue_shared/components/form/title.vue';
import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
-import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
+import { __, sprintf } from '~/locale';
import {
SNIPPET_MARK_EDIT_APP_START,
SNIPPET_MEASURE_BLOBS_CONTENT,
} from '~/performance/constants';
-import eventHub from '~/blob/components/eventhub';
import { performanceMarkAndMeasure } from '~/performance/utils';
+import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
+import TitleField from '~/vue_shared/components/form/title.vue';
-import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
-import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
-import { getSnippetMixin } from '../mixins/snippets';
import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '../constants';
+import { getSnippetMixin } from '../mixins/snippets';
+import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
+import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import { markBlobPerformance } from '../utils/blob';
+import { getErrorMessage } from '../utils/error';
import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
-import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
import SnippetDescriptionEdit from './snippet_description_edit.vue';
+import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
eventHub.$on(SNIPPET_MEASURE_BLOBS_CONTENT, markBlobPerformance);
@@ -32,6 +33,7 @@ export default {
SnippetBlobActionsEdit,
TitleField,
FormFooterActions,
+ CaptchaModal: () => import('~/captcha/captcha_modal.vue'),
GlButton,
GlLoadingIcon,
},
@@ -66,12 +68,25 @@ export default {
description: '',
visibilityLevel: this.selectedLevel,
},
+ captchaResponse: '',
+ needsCaptchaResponse: false,
+ captchaSiteKey: '',
+ spamLogId: '',
};
},
computed: {
hasBlobChanges() {
return this.actions.length > 0;
},
+ hasNoChanges() {
+ return (
+ this.actions.every(
+ (action) => !action.content && !action.filePath && !action.previousPath,
+ ) &&
+ !this.snippet.title &&
+ !this.snippet.description
+ );
+ },
hasValidBlobs() {
return this.actions.every((x) => x.content);
},
@@ -88,6 +103,8 @@ export default {
description: this.snippet.description,
visibilityLevel: this.snippet.visibilityLevel,
blobActions: this.actions,
+ ...(this.spamLogId && { spamLogId: this.spamLogId }),
+ ...(this.captchaResponse && { captchaResponse: this.captchaResponse }),
};
},
saveButtonLabel() {
@@ -116,7 +133,7 @@ export default {
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
- if (!this.hasBlobChanges || this.isUpdating) return undefined;
+ if (!this.hasBlobChanges || this.hasNoChanges || this.isUpdating) return undefined;
Object.assign(e, { returnValue });
return returnValue;
@@ -159,6 +176,13 @@ export default {
.then(({ data }) => {
const baseObj = this.newSnippet ? data?.createSnippet : data?.updateSnippet;
+ if (baseObj.needsCaptchaResponse) {
+ // If we need a captcha response, start process for receiving captcha response.
+ // We will resubmit after the response is obtained.
+ this.requestCaptchaResponse(baseObj.captchaSiteKey, baseObj.spamLogId);
+ return;
+ }
+
const errors = baseObj?.errors;
if (errors.length) {
this.flashAPIFailure(errors[0]);
@@ -167,12 +191,44 @@ export default {
}
})
.catch((e) => {
- this.flashAPIFailure(e);
+ // eslint-disable-next-line no-console
+ console.error('[gitlab] unexpected error while updating snippet', e);
+
+ this.flashAPIFailure(getErrorMessage(e));
});
},
updateActions(actions) {
this.actions = actions;
},
+ /**
+ * Start process for getting captcha response from user
+ *
+ * @param captchaSiteKey Stored in data and used to display the captcha.
+ * @param spamLogId Stored in data and included when the form is re-submitted.
+ */
+ requestCaptchaResponse(captchaSiteKey, spamLogId) {
+ this.captchaSiteKey = captchaSiteKey;
+ this.spamLogId = spamLogId;
+ this.needsCaptchaResponse = true;
+ },
+ /**
+ * Handle the captcha response from the user
+ *
+ * @param captchaResponse The captchaResponse value emitted from the modal.
+ */
+ receivedCaptchaResponse(captchaResponse) {
+ this.needsCaptchaResponse = false;
+ this.captchaResponse = captchaResponse;
+
+ if (this.captchaResponse) {
+ // If the user solved the captcha resubmit the form.
+ this.handleFormSubmit();
+ } else {
+ // If the user didn't solve the captcha (e.g. they just closed the modal),
+ // finish the update and allow them to continue editing or manually resubmit the form.
+ this.isUpdating = false;
+ }
+ },
},
};
</script>
@@ -190,6 +246,11 @@ export default {
class="loading-animation prepend-top-20 gl-mb-6"
/>
<template v-else>
+ <captcha-modal
+ :captcha-site-key="captchaSiteKey"
+ :needs-captcha-response="needsCaptchaResponse"
+ @receivedCaptchaResponse="receivedCaptchaResponse"
+ />
<title-field
id="snippet-title"
v-model="snippet.title"
diff --git a/app/assets/javascripts/snippets/components/embed_dropdown.vue b/app/assets/javascripts/snippets/components/embed_dropdown.vue
index a5d2c337d67..f6c9c569b5f 100644
--- a/app/assets/javascripts/snippets/components/embed_dropdown.vue
+++ b/app/assets/javascripts/snippets/components/embed_dropdown.vue
@@ -1,5 +1,4 @@
<script>
-import { escape as esc } from 'lodash';
import {
GlButton,
GlDropdown,
@@ -8,6 +7,7 @@ import {
GlFormInputGroup,
GlTooltipDirective,
} from '@gitlab/ui';
+import { escape as esc } from 'lodash';
import { __ } from '~/locale';
const MSG_EMBED = __('Embed');
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index a3e5535c5fa..46629a569ec 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -1,20 +1,20 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import EmbedDropdown from './embed_dropdown.vue';
-import SnippetHeader from './snippet_header.vue';
-import SnippetTitle from './snippet_title.vue';
-import SnippetBlob from './snippet_blob_view.vue';
-import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
-import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
+import eventHub from '~/blob/components/eventhub';
import {
SNIPPET_MARK_VIEW_APP_START,
SNIPPET_MEASURE_BLOBS_CONTENT,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
-import eventHub from '~/blob/components/eventhub';
+import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
+import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
import { getSnippetMixin } from '../mixins/snippets';
import { markBlobPerformance } from '../utils/blob';
+import EmbedDropdown from './embed_dropdown.vue';
+import SnippetBlob from './snippet_blob_view.vue';
+import SnippetHeader from './snippet_header.vue';
+import SnippetTitle from './snippet_title.vue';
eventHub.$on(SNIPPET_MEASURE_BLOBS_CONTENT, markBlobPerformance);
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
index ff27c90a84d..d221195ddc7 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
@@ -2,9 +2,9 @@
import { GlButton } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import { s__, sprintf } from '~/locale';
-import SnippetBlobEdit from './snippet_blob_edit.vue';
import { SNIPPET_MAX_BLOBS } from '../constants';
import { createBlob, decorateBlob, diffAll } from '../utils/blob';
+import SnippetBlobEdit from './snippet_blob_edit.vue';
export default {
components: {
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index c8545e334a6..4fb27397039 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -1,12 +1,12 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
-import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
-import axios from '~/lib/utils/axios_utils';
-import { SNIPPET_BLOB_CONTENT_FETCH_ERROR } from '~/snippets/constants';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
import { sprintf } from '~/locale';
+import { SNIPPET_BLOB_CONTENT_FETCH_ERROR } from '~/snippets/constants';
+import EditorLite from '~/vue_shared/components/editor_lite.vue';
export default {
components: {
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index 4326c3c3159..27b3a30b40a 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -1,8 +1,8 @@
<script>
import GetBlobContent from 'shared_queries/snippet/snippet_blob_content.query.graphql';
-import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContent from '~/blob/components/blob_content.vue';
+import BlobHeader from '~/blob/components/blob_header.vue';
import {
SIMPLE_BLOB_VIEWER,
@@ -31,8 +31,10 @@ export default {
},
result() {
if (this.activeViewerType === RICH_BLOB_VIEWER) {
+ // eslint-disable-next-line vue/no-mutating-props
this.blob.richViewer.renderError = null;
} else {
+ // eslint-disable-next-line vue/no-mutating-props
this.blob.simpleViewer.renderError = null;
}
},
diff --git a/app/assets/javascripts/snippets/components/snippet_description_edit.vue b/app/assets/javascripts/snippets/components/snippet_description_edit.vue
index 5e6caf27bdd..bac423f6838 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_edit.vue
@@ -1,7 +1,7 @@
<script>
import { GlFormInput } from '@gitlab/ui';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import setupCollapsibleInputs from '~/snippet/collapsible_input';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
export default {
components: {
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 5ba62908b43..bf19b63650e 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -11,14 +11,14 @@ import {
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
-import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql';
import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql';
+import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql';
+import { fetchPolicies } from '~/lib/graphql';
+import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { fetchPolicies } from '~/lib/graphql';
export default {
components: {
diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
index ee5076835ab..18a7d4ad218 100644
--- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
-import { defaultSnippetVisibilityLevels } from '../utils/blob';
import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants';
+import { defaultSnippetVisibilityLevels } from '../utils/blob';
export default {
components: {
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
index 853ccb0c2fe..789332ce5b7 100644
--- a/app/assets/javascripts/snippets/index.js
+++ b/app/assets/javascripts/snippets/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import Translate from '~/vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
import { SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
+import Translate from '~/vue_shared/translate';
Vue.use(VueApollo);
Vue.use(Translate);
diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js
index 89a88958152..7552eae97fc 100644
--- a/app/assets/javascripts/snippets/mixins/snippets.js
+++ b/app/assets/javascripts/snippets/mixins/snippets.js
@@ -11,10 +11,14 @@ export const getSnippetMixin = {
ids: [this.snippetGid],
};
},
- update: (data) => {
+ update(data) {
const res = data.snippets.nodes[0];
+
+ // Set `snippet.blobs` since some child components are coupled to this.
if (res) {
- res.blobs = res.blobs.nodes;
+ // It's possible for us to not get any blobs in a response.
+ // In this case, we should default to current blobs.
+ res.blobs = res.blobs ? res.blobs.nodes : this.blobs;
}
return res;
diff --git a/app/assets/javascripts/snippets/mutations/createSnippet.mutation.graphql b/app/assets/javascripts/snippets/mutations/createSnippet.mutation.graphql
index f688868d1b9..64d5d7c30fa 100644
--- a/app/assets/javascripts/snippets/mutations/createSnippet.mutation.graphql
+++ b/app/assets/javascripts/snippets/mutations/createSnippet.mutation.graphql
@@ -4,5 +4,7 @@ mutation CreateSnippet($input: CreateSnippetInput!) {
snippet {
webUrl
}
+ needsCaptchaResponse
+ captchaSiteKey
}
}
diff --git a/app/assets/javascripts/snippets/mutations/updateSnippet.mutation.graphql b/app/assets/javascripts/snippets/mutations/updateSnippet.mutation.graphql
index 548725f7357..0a72f71b7c9 100644
--- a/app/assets/javascripts/snippets/mutations/updateSnippet.mutation.graphql
+++ b/app/assets/javascripts/snippets/mutations/updateSnippet.mutation.graphql
@@ -4,5 +4,8 @@ mutation UpdateSnippet($input: UpdateSnippetInput!) {
snippet {
webUrl
}
+ needsCaptchaResponse
+ captchaSiteKey
+ spamLogId
}
}
diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js
index a47418323f2..2a3f590a803 100644
--- a/app/assets/javascripts/snippets/utils/blob.js
+++ b/app/assets/javascripts/snippets/utils/blob.js
@@ -1,4 +1,6 @@
import { uniqueId } from 'lodash';
+import { SNIPPET_MARK_BLOBS_CONTENT, SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
+import { performanceMarkAndMeasure } from '~/performance/utils';
import {
SNIPPET_BLOB_ACTION_CREATE,
SNIPPET_BLOB_ACTION_UPDATE,
@@ -7,8 +9,6 @@ import {
SNIPPET_LEVELS_MAP,
SNIPPET_VISIBILITY,
} from '../constants';
-import { performanceMarkAndMeasure } from '~/performance/utils';
-import { SNIPPET_MARK_BLOBS_CONTENT, SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
const createLocalId = () => uniqueId('blob_local_');
diff --git a/app/assets/javascripts/snippets/utils/error.js b/app/assets/javascripts/snippets/utils/error.js
new file mode 100644
index 00000000000..2d5c2a64038
--- /dev/null
+++ b/app/assets/javascripts/snippets/utils/error.js
@@ -0,0 +1,15 @@
+import { isString } from 'lodash';
+import { __ } from '~/locale';
+
+export const UNEXPECTED_ERROR = __('Unexpected error');
+
+export const getErrorMessage = (e) => {
+ if (!e) {
+ return UNEXPECTED_ERROR;
+ }
+ if (isString(e)) {
+ return e;
+ }
+
+ return e.message || e.networkError || UNEXPECTED_ERROR;
+};
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 232c97ecae0..eb3eaa66df5 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { deprecatedCreateFlash as Flash } from './flash';
-import { __, s__ } from './locale';
-import { spriteIcon } from './lib/utils/common_utils';
import axios from './lib/utils/axios_utils';
+import { spriteIcon } from './lib/utils/common_utils';
+import { __, s__ } from './locale';
export default class Star {
constructor(container = '.project-home-panel') {
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index b47126cdeb3..a51a4f9f604 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -1,15 +1,15 @@
<script>
-import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
-import PublishToolbar from './publish_toolbar.vue';
-import EditHeader from './edit_header.vue';
-import EditDrawer from './edit_drawer.vue';
-import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import imageRepository from '../image_repository';
import formatter from '../services/formatter';
-import templater from '../services/templater';
import renderImage from '../services/renderers/render_image';
+import templater from '../services/templater';
+import EditDrawer from './edit_drawer.vue';
+import EditHeader from './edit_header.vue';
+import PublishToolbar from './publish_toolbar.vue';
+import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
export default {
components: {
diff --git a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
index 0484d38dde0..0685dfdb1d1 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
@@ -22,11 +22,6 @@ export default {
<template>
<gl-drawer class="gl-pt-8" :open="isOpen" @close="$emit('close')">
<template #header>{{ __('Page settings') }}</template>
- <template>
- <front-matter-controls
- :settings="settings"
- @updateSettings="$emit('updateSettings', $event)"
- />
- </template>
+ <front-matter-controls :settings="settings" @updateSettings="$emit('updateSettings', $event)" />
</gl-drawer>
</template>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue b/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue
index f583d2049af..8c3ee7b9609 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_meta_modal.vue
@@ -1,12 +1,11 @@
<script>
import { GlModal } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
import Api from '~/api';
+import { __, s__, sprintf } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import EditMetaControls from './edit_meta_controls.vue';
-
import { ISSUABLE_TYPE, MR_META_LOCAL_STORAGE_KEY } from '../constants';
+import EditMetaControls from './edit_meta_controls.vue';
export default {
components: {
diff --git a/app/assets/javascripts/static_site_editor/graphql/index.js b/app/assets/javascripts/static_site_editor/graphql/index.js
index bce320ed805..23f800517c9 100644
--- a/app/assets/javascripts/static_site_editor/graphql/index.js
+++ b/app/assets/javascripts/static_site_editor/graphql/index.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import typeDefs from './typedefs.graphql';
import fileResolver from './resolvers/file';
-import submitContentChangesResolver from './resolvers/submit_content_changes';
import hasSubmittedChangesResolver from './resolvers/has_submitted_changes';
+import submitContentChangesResolver from './resolvers/submit_content_changes';
+import typeDefs from './typedefs.graphql';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/static_site_editor/image_repository.js b/app/assets/javascripts/static_site_editor/image_repository.js
index 56b2434d2e2..57f32ab4847 100644
--- a/app/assets/javascripts/static_site_editor/image_repository.js
+++ b/app/assets/javascripts/static_site_editor/image_repository.js
@@ -1,5 +1,5 @@
-import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import { __ } from '~/locale';
import { getBinary } from './services/image_service';
const imageRepository = () => {
diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js
index fbb14be21ba..985579f68e8 100644
--- a/app/assets/javascripts/static_site_editor/index.js
+++ b/app/assets/javascripts/static_site_editor/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import App from './components/app.vue';
-import createRouter from './router';
import createApolloProvider from './graphql';
+import createRouter from './router';
const initStaticSiteEditor = (el) => {
const {
diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue
index 6c958cb2d22..22f80ead74b 100644
--- a/app/assets/javascripts/static_site_editor/pages/home.vue
+++ b/app/assets/javascripts/static_site_editor/pages/home.vue
@@ -2,16 +2,16 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import Tracking from '~/tracking';
-import SkeletonLoader from '../components/skeleton_loader.vue';
import EditArea from '../components/edit_area.vue';
import EditMetaModal from '../components/edit_meta_modal.vue';
import InvalidContentMessage from '../components/invalid_content_message.vue';
+import SkeletonLoader from '../components/skeleton_loader.vue';
import SubmitChangesError from '../components/submit_changes_error.vue';
-import appDataQuery from '../graphql/queries/app_data.query.graphql';
-import sourceContentQuery from '../graphql/queries/source_content.query.graphql';
+import { LOAD_CONTENT_ERROR, TRACKING_ACTION_INITIALIZE_EDITOR } from '../constants';
import hasSubmittedChangesMutation from '../graphql/mutations/has_submitted_changes.mutation.graphql';
import submitContentChangesMutation from '../graphql/mutations/submit_content_changes.mutation.graphql';
-import { LOAD_CONTENT_ERROR, TRACKING_ACTION_INITIALIZE_EDITOR } from '../constants';
+import appDataQuery from '../graphql/queries/app_data.query.graphql';
+import sourceContentQuery from '../graphql/queries/source_content.query.graphql';
import { SUCCESS_ROUTE } from '../router/constants';
export default {
diff --git a/app/assets/javascripts/static_site_editor/pages/success.vue b/app/assets/javascripts/static_site_editor/pages/success.vue
index 1ee1a3b0edf..70e692a0c86 100644
--- a/app/assets/javascripts/static_site_editor/pages/success.vue
+++ b/app/assets/javascripts/static_site_editor/pages/success.vue
@@ -2,8 +2,8 @@
import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
-import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
+import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
import { HOME_ROUTE } from '../router/constants';
export default {
diff --git a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
index 84e90deacfc..6391cfd6cc2 100644
--- a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
+++ b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
@@ -1,7 +1,7 @@
import Api from '~/api';
-import Tracking from '~/tracking';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
+import Tracking from '~/tracking';
import {
DEFAULT_TARGET_BRANCH,
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 354ee00a977..4a688d819b0 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { __ } from './locale';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { __ } from './locale';
export default function subscriptionSelect() {
$('.js-subscription-event').each((i, element) => {
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index 81d9d9d37a7..bc8a8e425dd 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import 'deckar01-task_list';
import { __ } from '~/locale';
-import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as Flash } from './flash';
+import axios from './lib/utils/axios_utils';
export default class TaskList {
constructor(options = {}) {
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
index 22bbd083a5d..1bb5e214c2e 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -1,16 +1,15 @@
/* eslint-disable no-useless-return */
import $ from 'jquery';
+import { __ } from '~/locale';
import Api from '../api';
import TemplateSelector from '../blob/template_selector';
-import { __ } from '~/locale';
export default class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) {
super(...args);
- this.projectPath = this.dropdown.data('projectPath');
- this.namespacePath = this.dropdown.data('namespacePath');
+ this.projectId = this.dropdown.data('projectId');
this.issuableType = this.$dropdownContainer.data('issuableType');
this.titleInput = $(`#${this.issuableType}_title`);
this.templateWarningEl = $('.js-issuable-template-warning');
@@ -81,21 +80,21 @@ export default class IssuableTemplateSelector extends TemplateSelector {
}
requestFile(query) {
+ const callback = (currentTemplate) => {
+ this.currentTemplate = currentTemplate;
+ this.stopLoadingSpinner();
+ this.setInputValueToTemplateContent();
+ };
+
this.startLoadingSpinner();
- Api.issueTemplate(
- this.namespacePath,
- this.projectPath,
- query.name,
+ Api.projectTemplate(
+ this.projectId,
this.issuableType,
- (err, currentTemplate) => {
- this.currentTemplate = currentTemplate;
- this.stopLoadingSpinner();
- if (err) return; // Error handled by global AJAX error handler
- this.setInputValueToTemplateContent();
- },
+ query.name,
+ { source_template_project_id: query.project_id },
+ callback,
);
- return;
}
setInputValueToTemplateContent() {
diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js
index 26cc538994f..468f9b54ee0 100644
--- a/app/assets/javascripts/terminal/terminal.js
+++ b/app/assets/javascripts/terminal/terminal.js
@@ -1,5 +1,5 @@
-import { throttle } from 'lodash';
import $ from 'jquery';
+import { throttle } from 'lodash';
import { Terminal } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
import * as webLinks from 'xterm/lib/addons/webLinks/webLinks';
diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue
index d0d49233334..2577664a5e8 100644
--- a/app/assets/javascripts/terraform/components/states_table.vue
+++ b/app/assets/javascripts/terraform/components/states_table.vue
@@ -1,18 +1,29 @@
<script>
-import { GlBadge, GlIcon, GlLink, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import {
+ GlAlert,
+ GlBadge,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlSprintf,
+ GlTable,
+ GlTooltip,
+} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { s__ } from '~/locale';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
-import StateActions from './states_table_actions.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import StateActions from './states_table_actions.vue';
export default {
components: {
CiBadge,
+ GlAlert,
GlBadge,
GlIcon,
GlLink,
+ GlLoadingIcon,
GlSprintf,
GlTable,
GlTooltip,
@@ -66,15 +77,21 @@ export default {
jobStatus: s__('Terraform|Job status'),
locked: s__('Terraform|Locked'),
lockedByUser: s__('Terraform|Locked by %{user} %{timeAgo}'),
+ lockingState: s__('Terraform|Locking state'),
name: s__('Terraform|Name'),
pipeline: s__('Terraform|Pipeline'),
+ removing: s__('Terraform|Removing'),
unknownUser: s__('Terraform|Unknown User'),
+ unlockingState: s__('Terraform|Unlocking state'),
updatedUser: s__('Terraform|%{user} updated %{timeAgo}'),
},
methods: {
createdByUserName(item) {
return item.latestVersion?.createdByUser?.name;
},
+ loadingLockText(item) {
+ return item.lockedAt ? this.$options.i18n.unlockingState : this.$options.i18n.lockingState;
+ },
lockedByUserName(item) {
return item.lockedByUser?.name || this.$options.i18n.unknownUser;
},
@@ -105,6 +122,7 @@ export default {
:items="states"
:fields="fields"
data-testid="terraform-states-table"
+ details-td-class="gl-p-0!"
fixed
stacked="md"
>
@@ -117,7 +135,27 @@ export default {
{{ item.name }}
</p>
- <div v-if="item.lockedAt" :id="`terraformLockedBadgeContainer${item.name}`" class="gl-mx-2">
+ <div v-if="item.loadingLock" class="gl-mx-3">
+ <p class="gl-display-flex gl-justify-content-start gl-align-items-baseline gl-m-0">
+ <gl-loading-icon class="gl-pr-1" />
+ {{ loadingLockText(item) }}
+ </p>
+ </div>
+
+ <div v-else-if="item.loadingRemove" class="gl-mx-3">
+ <p
+ class="gl-display-flex gl-justify-content-start gl-align-items-baseline gl-m-0 gl-text-red-500"
+ >
+ <gl-loading-icon class="gl-pr-1" />
+ {{ $options.i18n.removing }}
+ </p>
+ </div>
+
+ <div
+ v-else-if="item.lockedAt"
+ :id="`terraformLockedBadgeContainer${item.name}`"
+ class="gl-mx-3"
+ >
<gl-badge :id="`terraformLockedBadge${item.name}`">
<gl-icon name="lock" />
{{ $options.i18n.locked }}
@@ -189,5 +227,21 @@ export default {
<template v-if="terraformAdmin" #cell(actions)="{ item }">
<state-actions :state="item" />
</template>
+
+ <template #row-details="row">
+ <gl-alert
+ data-testid="terraform-states-table-error"
+ variant="danger"
+ @dismiss="row.toggleDetails"
+ >
+ <span
+ v-for="errorMessage in row.item.errorMessages"
+ :key="errorMessage"
+ class="gl-display-flex gl-justify-content-start"
+ >
+ {{ errorMessage }}
+ </span>
+ </gl-alert>
+ </template>
</gl-table>
</template>
diff --git a/app/assets/javascripts/terraform/components/states_table_actions.vue b/app/assets/javascripts/terraform/components/states_table_actions.vue
index 44b0713e544..c4fd97188de 100644
--- a/app/assets/javascripts/terraform/components/states_table_actions.vue
+++ b/app/assets/javascripts/terraform/components/states_table_actions.vue
@@ -9,10 +9,11 @@ import {
GlModal,
GlSprintf,
} from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { s__, sprintf } from '~/locale';
+import addDataToState from '../graphql/mutations/add_data_to_state.mutation.graphql';
import lockState from '../graphql/mutations/lock_state.mutation.graphql';
-import unlockState from '../graphql/mutations/unlock_state.mutation.graphql';
import removeState from '../graphql/mutations/remove_state.mutation.graphql';
+import unlockState from '../graphql/mutations/unlock_state.mutation.graphql';
export default {
components: {
@@ -33,13 +34,13 @@ export default {
},
data() {
return {
- loading: false,
showRemoveModal: false,
removeConfirmText: '',
};
},
i18n: {
downloadJSON: s__('Terraform|Download JSON'),
+ errorUpdate: s__('Terraform|An error occurred while changing the state file'),
lock: s__('Terraform|Lock'),
modalBody: s__(
'Terraform|You are about to remove the State file %{name}. This will permanently delete all the State versions and history. The infrastructure provisioned previously will remain intact, only the state file with all its versions are to be removed. This action is non-revertible.',
@@ -51,6 +52,7 @@ export default {
),
modalRemove: s__('Terraform|Remove'),
remove: s__('Terraform|Remove state file and versions'),
+ removeSuccessful: s__('Terraform|%{name} successfully removed'),
unlock: s__('Terraform|Unlock'),
},
computed: {
@@ -63,6 +65,9 @@ export default {
disableModalSubmit() {
return this.removeConfirmText !== this.state.name;
},
+ loading() {
+ return this.state.loadingLock || this.state.loadingRemove;
+ },
primaryModalProps() {
return {
text: this.$options.i18n.modalRemove,
@@ -76,19 +81,56 @@ export default {
this.removeConfirmText = '';
},
lock() {
- this.stateMutation(lockState);
+ this.updateStateCache({
+ _showDetails: false,
+ errorMessages: [],
+ loadingLock: true,
+ loadingRemove: false,
+ });
+
+ this.stateActionMutation(lockState);
},
unlock() {
- this.stateMutation(unlockState);
+ this.updateStateCache({
+ _showDetails: false,
+ errorMessages: [],
+ loadingLock: true,
+ loadingRemove: false,
+ });
+
+ this.stateActionMutation(unlockState);
+ },
+ updateStateCache(newData) {
+ this.$apollo.mutate({
+ mutation: addDataToState,
+ variables: {
+ terraformState: {
+ ...this.state,
+ ...newData,
+ },
+ },
+ });
},
remove() {
if (!this.disableModalSubmit) {
this.hideModal();
- this.stateMutation(removeState);
+
+ this.updateStateCache({
+ _showDetails: false,
+ errorMessages: [],
+ loadingLock: false,
+ loadingRemove: true,
+ });
+
+ this.stateActionMutation(
+ removeState,
+ sprintf(this.$options.i18n.removeSuccessful, { name: this.state.name }),
+ );
}
},
- stateMutation(mutation) {
- this.loading = true;
+ stateActionMutation(mutation, successMessage = null) {
+ let errorMessages = [];
+
this.$apollo
.mutate({
mutation,
@@ -99,9 +141,27 @@ export default {
awaitRefetchQueries: true,
notifyOnNetworkStatusChange: true,
})
- .catch(() => {})
+ .then(({ data }) => {
+ errorMessages =
+ data?.terraformStateDelete?.errors ||
+ data?.terraformStateLock?.errors ||
+ data?.terraformStateUnlock?.errors ||
+ [];
+
+ if (errorMessages.length === 0 && successMessage) {
+ this.$toast.show(successMessage);
+ }
+ })
+ .catch(() => {
+ errorMessages = [this.$options.i18n.errorUpdate];
+ })
.finally(() => {
- this.loading = false;
+ this.updateStateCache({
+ _showDetails: Boolean(errorMessages.length),
+ errorMessages,
+ loadingLock: false,
+ loadingRemove: false,
+ });
});
},
},
diff --git a/app/assets/javascripts/terraform/components/terraform_list.vue b/app/assets/javascripts/terraform/components/terraform_list.vue
index b71133d8813..a18f33ebb1f 100644
--- a/app/assets/javascripts/terraform/components/terraform_list.vue
+++ b/app/assets/javascripts/terraform/components/terraform_list.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert, GlBadge, GlKeysetPagination, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
+import { MAX_LIST_COUNT } from '../constants';
import getStatesQuery from '../graphql/queries/get_states.query.graphql';
import EmptyState from './empty_state.vue';
import StatesTable from './states_table.vue';
-import { MAX_LIST_COUNT } from '../constants';
export default {
apollo: {
diff --git a/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql b/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql
index 49f9ae3dd97..fb823336411 100644
--- a/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql
+++ b/app/assets/javascripts/terraform/graphql/fragments/state.fragment.graphql
@@ -2,6 +2,11 @@
#import "./state_version.fragment.graphql"
fragment State on TerraformState {
+ _showDetails @client
+ errorMessages @client
+ loadingLock @client
+ loadingRemove @client
+
id
name
lockedAt
diff --git a/app/assets/javascripts/terraform/graphql/mutations/add_data_to_state.mutation.graphql b/app/assets/javascripts/terraform/graphql/mutations/add_data_to_state.mutation.graphql
new file mode 100644
index 00000000000..645b9766e2b
--- /dev/null
+++ b/app/assets/javascripts/terraform/graphql/mutations/add_data_to_state.mutation.graphql
@@ -0,0 +1,3 @@
+mutation addDataToTerraformState($terraformState: State!) {
+ addDataToTerraformState(terraformState: $terraformState) @client
+}
diff --git a/app/assets/javascripts/terraform/graphql/resolvers.js b/app/assets/javascripts/terraform/graphql/resolvers.js
new file mode 100644
index 00000000000..d27379bdd9f
--- /dev/null
+++ b/app/assets/javascripts/terraform/graphql/resolvers.js
@@ -0,0 +1,45 @@
+import TerraformState from './fragments/state.fragment.graphql';
+
+export default {
+ TerraformState: {
+ _showDetails: (state) => {
+ // eslint-disable-next-line no-underscore-dangle
+ return state._showDetails || false;
+ },
+ errorMessages: (state) => {
+ return state.errorMessages || [];
+ },
+ loadingLock: (state) => {
+ return state.loadingLock || false;
+ },
+ loadingRemove: (state) => {
+ return state.loadingRemove || false;
+ },
+ },
+ Mutation: {
+ addDataToTerraformState: (_, { terraformState }, { client }) => {
+ const fragmentData = {
+ id: terraformState.id,
+ fragment: TerraformState,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ fragmentName: 'State',
+ };
+
+ const previousTerraformState = client.readFragment(fragmentData);
+
+ if (previousTerraformState) {
+ client.writeFragment({
+ ...fragmentData,
+ data: {
+ ...previousTerraformState,
+ // eslint-disable-next-line no-underscore-dangle
+ _showDetails: terraformState._showDetails,
+ errorMessages: terraformState.errorMessages,
+ loadingLock: terraformState.loadingLock,
+ loadingRemove: terraformState.loadingRemove,
+ },
+ });
+ }
+ },
+ },
+};
diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js
index e27a29433f3..3f986423836 100644
--- a/app/assets/javascripts/terraform/index.js
+++ b/app/assets/javascripts/terraform/index.js
@@ -1,8 +1,12 @@
+import { GlToast } from '@gitlab/ui';
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import TerraformList from './components/terraform_list.vue';
import createDefaultClient from '~/lib/graphql';
+import TerraformList from './components/terraform_list.vue';
+import resolvers from './graphql/resolvers';
+Vue.use(GlToast);
Vue.use(VueApollo);
export default () => {
@@ -12,7 +16,13 @@ export default () => {
return null;
}
- const defaultClient = createDefaultClient();
+ const defaultClient = createDefaultClient(resolvers, {
+ cacheConfig: {
+ dataIdFromObject: (object) => {
+ return object.id || defaultDataIdFromObject(object);
+ },
+ },
+ });
const { emptyStateImage, projectPath } = el.dataset;
diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js
index 2f933823842..03c975d5fe8 100644
--- a/app/assets/javascripts/toggle_buttons.js
+++ b/app/assets/javascripts/toggle_buttons.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { deprecatedCreateFlash as Flash } from './flash';
-import { __ } from './locale';
import { parseBoolean } from './lib/utils/common_utils';
+import { __ } from './locale';
/*
example HAML:
diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js
index b216affc818..a9978c03a6e 100644
--- a/app/assets/javascripts/tooltips/index.js
+++ b/app/assets/javascripts/tooltips/index.js
@@ -1,6 +1,5 @@
+import { toArray, isElement } from 'lodash';
import Vue from 'vue';
-import jQuery from 'jquery';
-import { toArray, isFunction, isElement } from 'lodash';
import Tooltips from './components/tooltips.vue';
let app;
@@ -60,72 +59,39 @@ const applyToElements = (elements, handler) => {
toArray(iterable).forEach(handler);
};
-const invokeBootstrapApi = (elements, method) => {
- if (isFunction(elements.tooltip)) {
- elements.tooltip(method);
- } else {
- jQuery(elements).tooltip(method);
- }
-};
-
-const isGlTooltipsEnabled = () => Boolean(window.gon.features?.glTooltips);
-
-const tooltipApiInvoker = ({ glHandler, bsHandler }) => (elements, ...params) => {
- if (isGlTooltipsEnabled()) {
- applyToElements(elements, glHandler);
- } else {
- bsHandler(elements, ...params);
- }
+const createTooltipApiInvoker = (glHandler) => (elements) => {
+ applyToElements(elements, glHandler);
};
export const initTooltips = (config = {}) => {
- if (isGlTooltipsEnabled()) {
- const triggers = config?.triggers || DEFAULT_TRIGGER;
- const events = triggers.split(' ').map((trigger) => EVENTS_MAP[trigger]);
-
- events.forEach((event) => {
- document.addEventListener(
- event,
- (e) => handleTooltipEvent(document, e, config.selector, config),
- true,
- );
- });
-
- return tooltipsApp();
- }
-
- return invokeBootstrapApi(document.body, config);
-};
-export const add = (elements, config = {}) => {
- if (isGlTooltipsEnabled()) {
- return addTooltips(elements, config);
- }
- return invokeBootstrapApi(elements, config);
+ const triggers = config?.triggers || DEFAULT_TRIGGER;
+ const events = triggers.split(' ').map((trigger) => EVENTS_MAP[trigger]);
+
+ events.forEach((event) => {
+ document.addEventListener(
+ event,
+ (e) => handleTooltipEvent(document, e, config.selector, config),
+ true,
+ );
+ });
+
+ return tooltipsApp();
};
-export const dispose = tooltipApiInvoker({
- glHandler: (element) => tooltipsApp().dispose(element),
- bsHandler: (elements) => invokeBootstrapApi(elements, 'dispose'),
-});
-export const fixTitle = tooltipApiInvoker({
- glHandler: (element) => tooltipsApp().fixTitle(element),
- bsHandler: (elements) => invokeBootstrapApi(elements, '_fixTitle'),
-});
-export const enable = tooltipApiInvoker({
- glHandler: (element) => tooltipsApp().triggerEvent(element, 'enable'),
- bsHandler: (elements) => invokeBootstrapApi(elements, 'enable'),
-});
-export const disable = tooltipApiInvoker({
- glHandler: (element) => tooltipsApp().triggerEvent(element, 'disable'),
- bsHandler: (elements) => invokeBootstrapApi(elements, 'disable'),
-});
-export const hide = tooltipApiInvoker({
- glHandler: (element) => tooltipsApp().triggerEvent(element, 'close'),
- bsHandler: (elements) => invokeBootstrapApi(elements, 'hide'),
-});
-export const show = tooltipApiInvoker({
- glHandler: (element) => tooltipsApp().triggerEvent(element, 'open'),
- bsHandler: (elements) => invokeBootstrapApi(elements, 'show'),
-});
+export const add = (elements, config = {}) => addTooltips(elements, config);
+export const dispose = createTooltipApiInvoker((element) => tooltipsApp().dispose(element));
+export const fixTitle = createTooltipApiInvoker((element) => tooltipsApp().fixTitle(element));
+export const enable = createTooltipApiInvoker((element) =>
+ tooltipsApp().triggerEvent(element, 'enable'),
+);
+export const disable = createTooltipApiInvoker((element) =>
+ tooltipsApp().triggerEvent(element, 'disable'),
+);
+export const hide = createTooltipApiInvoker((element) =>
+ tooltipsApp().triggerEvent(element, 'close'),
+);
+export const show = createTooltipApiInvoker((element) =>
+ tooltipsApp().triggerEvent(element, 'open'),
+);
export const destroy = () => {
tooltipsApp().$destroy();
app = null;
diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js
index 028b047d9f5..1a3fd6c77ed 100644
--- a/app/assets/javascripts/ui_development_kit.js
+++ b/app/assets/javascripts/ui_development_kit.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import Api from './api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import Api from './api';
export default () => {
initDeprecatedJQueryDropdown($('#js-project-dropdown'), {
diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/usage_ping_consent.js
index 94d476d13ae..3876aa62b75 100644
--- a/app/assets/javascripts/usage_ping_consent.js
+++ b/app/assets/javascripts/usage_ping_consent.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import axios from './lib/utils/axios_utils';
import { deprecatedCreateFlash as Flash, hideFlash } from './flash';
+import axios from './lib/utils/axios_utils';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
diff --git a/app/assets/javascripts/user_lists/components/edit_user_list.vue b/app/assets/javascripts/user_lists/components/edit_user_list.vue
index d56c3d61027..18f411f6cf2 100644
--- a/app/assets/javascripts/user_lists/components/edit_user_list.vue
+++ b/app/assets/javascripts/user_lists/components/edit_user_list.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { s__, sprintf } from '~/locale';
import statuses from '../constants/edit';
import UserListForm from './user_list_form.vue';
diff --git a/app/assets/javascripts/user_lists/components/new_user_list.vue b/app/assets/javascripts/user_lists/components/new_user_list.vue
index 522e077fb25..17ef4c037d2 100644
--- a/app/assets/javascripts/user_lists/components/new_user_list.vue
+++ b/app/assets/javascripts/user_lists/components/new_user_list.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapState } from 'vuex';
import { GlAlert } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import UserListForm from './user_list_form.vue';
diff --git a/app/assets/javascripts/user_lists/components/user_list.vue b/app/assets/javascripts/user_lists/components/user_list.vue
index 0e2b72c1423..e33a4b3ffb4 100644
--- a/app/assets/javascripts/user_lists/components/user_list.vue
+++ b/app/assets/javascripts/user_lists/components/user_list.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapState } from 'vuex';
import {
GlAlert,
GlButton,
@@ -7,6 +6,7 @@ import {
GlLoadingIcon,
GlModalDirective as GlModal,
} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
import { s__, __ } from '~/locale';
import { states, ADD_USER_MODAL_ID } from '../constants/show';
import AddUserModal from './add_user_modal.vue';
diff --git a/app/assets/javascripts/user_lists/store/edit/index.js b/app/assets/javascripts/user_lists/store/edit/index.js
index 3b19b2b12ec..9b9df59ed32 100644
--- a/app/assets/javascripts/user_lists/store/edit/index.js
+++ b/app/assets/javascripts/user_lists/store/edit/index.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
-import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
+import createState from './state';
export default (initialState) =>
new Vuex.Store({
diff --git a/app/assets/javascripts/user_lists/store/new/index.js b/app/assets/javascripts/user_lists/store/new/index.js
index 3b19b2b12ec..9b9df59ed32 100644
--- a/app/assets/javascripts/user_lists/store/new/index.js
+++ b/app/assets/javascripts/user_lists/store/new/index.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
-import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
+import createState from './state';
export default (initialState) =>
new Vuex.Store({
diff --git a/app/assets/javascripts/user_lists/store/show/index.js b/app/assets/javascripts/user_lists/store/show/index.js
index 3b19b2b12ec..9b9df59ed32 100644
--- a/app/assets/javascripts/user_lists/store/show/index.js
+++ b/app/assets/javascripts/user_lists/store/show/index.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
-import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
+import createState from './state';
export default (initialState) =>
new Vuex.Store({
diff --git a/app/assets/javascripts/user_lists/store/show/mutations.js b/app/assets/javascripts/user_lists/store/show/mutations.js
index 3cf3b2d8371..bb5f9abe79e 100644
--- a/app/assets/javascripts/user_lists/store/show/mutations.js
+++ b/app/assets/javascripts/user_lists/store/show/mutations.js
@@ -1,6 +1,6 @@
import { states } from '../../constants/show';
-import * as types from './mutation_types';
import { parseUserIds } from '../utils';
+import * as types from './mutation_types';
export default {
[types.REQUEST_USER_LIST](state) {
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index 79dc20fd498..e1a4a74b982 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -8,14 +8,15 @@ import {
AJAX_USERS_SELECT_OPTIONS_MAP,
AJAX_USERS_SELECT_PARAMS_MAP,
} from 'ee_else_ce/users_select/constants';
-import axios from '../lib/utils/axios_utils';
-import { s__, __, sprintf } from '../locale';
-import ModalStore from '../boards/stores/modal_store';
-import { parseBoolean, spriteIcon } from '../lib/utils/common_utils';
-import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { isUserBusy } from '~/set_status_modal/utils';
import { fixTitle, dispose } from '~/tooltips';
+import ModalStore from '../boards/stores/modal_store';
+import axios from '../lib/utils/axios_utils';
+import { parseBoolean, spriteIcon } from '../lib/utils/common_utils';
import { loadCSSFile } from '../lib/utils/css_utils';
+import { s__, __, sprintf } from '../locale';
+import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
@@ -795,13 +796,17 @@ UsersSelect.prototype.renderRow = function (
? `data-container="body" data-placement="left" data-title="${tooltip}"`
: '';
+ const name =
+ user?.availability && isUserBusy(user.availability)
+ ? sprintf(__('%{name} (Busy)'), { name: user.name })
+ : user.name;
return `
<li data-user-id=${user.id}>
<a href="#" class="dropdown-menu-user-link d-flex align-items-center ${linkClasses}" ${tooltipAttributes}>
${this.renderRowAvatar(issuableType, user, img)}
<span class="d-flex flex-column overflow-hidden">
<strong class="dropdown-menu-user-full-name gl-font-weight-bold">
- ${escape(user.name)}
+ ${escape(name)}
</strong>
${
username
@@ -834,7 +839,7 @@ UsersSelect.prototype.renderRowAvatar = function (issuableType, user, img) {
UsersSelect.prototype.renderApprovalRules = function (elsClassName, approvalRules = []) {
const count = approvalRules.length;
- if (!gon.features?.reviewerApprovalRules || !elsClassName?.includes('reviewer') || !count) {
+ if (!elsClassName?.includes('reviewer') || !count) {
return '';
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
index 9b822657184..a5b83167283 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import eventHub from '../../event_hub';
import approvalsMixin from '../../mixins/approvals';
@@ -124,7 +125,7 @@ export default {
methods: {
approve() {
if (this.requirePasswordToApprove) {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
return;
}
@@ -158,6 +159,7 @@ export default {
.then((data) => {
this.mr.setApprovals(data);
eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('ApprovalUpdated');
this.$emit('updated');
})
.catch(errFn)
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
index fb342a5d340..ea73ab416de 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
@@ -1,8 +1,8 @@
<script>
-import { n__, sprintf } from '~/locale';
import { toNounSeriesText } from '~/lib/utils/grammar';
-import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
+import { n__, sprintf } from '~/locale';
import { APPROVED_MESSAGE } from '~/vue_merge_request_widget/components/approvals/messages';
+import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
export default {
components: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue
index 730e67761be..ebf42fa0be0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list_app.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
+import createStore from '../stores/artifacts_list';
import ArtifactsList from './artifacts_list.vue';
import MrCollapsibleExtension from './mr_collapsible_extension.vue';
-import createStore from '../stores/artifacts_list';
export default {
store: createStore(),
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
index f497936e299..d79da9d3b90 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
@@ -1,7 +1,7 @@
<script>
+import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED } from './constants';
import DeploymentActions from './deployment_actions.vue';
import DeploymentInfo from './deployment_info.vue';
-import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED } from './constants';
export default {
// name: 'Deployment' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
index 215df8acece..513d88ecab6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
@@ -1,11 +1,9 @@
<script>
-import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
+import { __, s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MRWidgetService from '../../services/mr_widget_service';
-import DeploymentActionButton from './deployment_action_button.vue';
-import DeploymentViewButton from './deployment_view_button.vue';
import {
MANUAL_DEPLOY,
FAILED,
@@ -15,6 +13,8 @@ import {
REDEPLOYING,
ACT_BUTTON_ICONS,
} from './constants';
+import DeploymentActionButton from './deployment_action_button.vue';
+import DeploymentViewButton from './deployment_view_button.vue';
export default {
name: 'DeploymentActions',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
index 390469dec24..cbace1ad57c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
@@ -3,7 +3,6 @@ import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import MemoryUsage from './memory_usage.vue';
import {
MANUAL_DEPLOY,
WILL_DEPLOY,
@@ -13,6 +12,7 @@ import {
CANCELED,
SKIPPED,
} from './constants';
+import MemoryUsage from './memory_usage.vue';
export default {
name: 'DeploymentInfo',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
index 2f27216f2e9..410d2740e1d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
@@ -1,9 +1,9 @@
<script>
import { GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { backOff } from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
-import { backOff } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
import MRWidgetService from '../../services/mr_widget_service';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 6628ab7be83..71ff0fe6fbd 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,6 +1,5 @@
<script>
/* eslint-disable vue/no-v-html */
-import { escape } from 'lodash';
import {
GlButton,
GlDropdown,
@@ -9,12 +8,13 @@ import {
GlTooltipDirective,
GlModalDirective,
} from '@gitlab/ui';
-import { n__, s__, sprintf } from '~/locale';
+import { escape } from 'lodash';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
+import { n__, s__, sprintf } from '~/locale';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
-import MrWidgetIcon from './mr_widget_icon.vue';
import MrWidgetHowToMergeModal from './mr_widget_how_to_merge_modal.vue';
+import MrWidgetIcon from './mr_widget_icon.vue';
export default {
name: 'MRWidgetHeader',
@@ -133,14 +133,12 @@ export default {
v-gl-tooltip
:title="ideButtonTitle"
class="gl-display-none d-md-inline-block gl-mr-3"
- :tabindex="!mr.canPushToSourceBranch ? 0 : null"
+ :tabindex="ideButtonTitle ? 0 : null"
>
<gl-button
:href="webIdePath"
:disabled="!mr.canPushToSourceBranch"
class="js-web-ide"
- tabindex="0"
- role="button"
data-qa-selector="open_in_web_ide_button"
>
{{ s__('mrWidget|Open in Web IDE') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
index 785e8ef8e8f..7c50df5f104 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable @gitlab/require-i18n-strings */
import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { __ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
i18n: {
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 4c130945487..d022579ef54 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,5 @@
<script>
-/* eslint-disable vue/require-default-prop, vue/no-v-html */
+/* eslint-disable vue/require-default-prop */
import {
GlIcon,
GlLink,
@@ -7,11 +7,12 @@ import {
GlSprintf,
GlTooltip,
GlTooltipDirective,
+ GlSafeHtmlDirective,
} from '@gitlab/ui';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
import { s__, n__ } from '~/locale';
-import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
+import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
@@ -32,6 +33,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
mixins: [mrWidgetPipelineMixin],
props: {
@@ -139,45 +141,38 @@ export default {
<div class="ci-widget media">
<template v-if="hasCIError">
<gl-icon name="status_failed" class="gl-text-red-500" :size="24" />
- <div
- class="gl-flex-fill-1 gl-ml-5"
- tabindex="0"
- role="text"
- :aria-label="$options.errorText"
- data-testid="ci-error-message"
- >
+ <p class="gl-flex-fill-1 gl-ml-5 gl-mb-0" data-testid="ci-error-message">
<gl-sprintf :message="$options.errorText">
<template #link="{ content }">
<gl-link :href="mrTroubleshootingDocsPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
- </div>
+ </p>
</template>
<template v-else-if="!hasPipeline">
<gl-loading-icon size="md" />
- <div class="gl-flex-fill-1 gl-display-flex gl-ml-5" data-testid="monitoring-pipeline-message">
- <span tabindex="0" role="text" :aria-label="$options.monitoringPipelineText">
- <gl-sprintf :message="$options.monitoringPipelineText" />
- </span>
+ <p
+ class="gl-flex-fill-1 gl-display-flex gl-ml-5 gl-mb-0"
+ data-testid="monitoring-pipeline-message"
+ >
+ {{ $options.monitoringPipelineText }}
<gl-link
+ v-gl-tooltip
:href="ciTroubleshootingDocsPath"
target="_blank"
+ :title="__('About this feature')"
class="gl-display-flex gl-align-items-center gl-ml-2"
- tabindex="0"
>
<gl-icon
name="question"
- :size="12"
- tabindex="0"
- role="text"
:aria-label="__('Link to go to GitLab pipeline documentation')"
/>
</gl-link>
- </div>
+ </p>
</template>
<template v-else-if="hasPipeline">
<a :href="status.details_path" class="align-self-start gl-mr-3">
- <ci-icon :status="status" :size="24" :borderless="true" class="add-border" />
+ <ci-icon :status="status" :size="24" />
</a>
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
@@ -208,10 +203,10 @@ export default {
<template v-if="showSourceBranch">
{{ s__('Pipeline|on') }}
<tooltip-on-truncate
+ v-safe-html="sourceBranchLink"
:title="sourceBranch"
truncate-target="child"
class="label-branch label-truncate gl-font-weight-normal"
- v-html="sourceBranchLink"
/>
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 99b55c0f9ee..2bf86c1863a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -1,10 +1,10 @@
<script>
import { isNumber } from 'lodash';
import { sanitize } from '~/lib/dompurify';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ArtifactsApp from './artifacts_list_app.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
/**
* Renders the pipeline and related deployments from the store.
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 bc23ca6b1fc..677c50ed930 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
@@ -43,10 +43,9 @@ export default {
<gl-button
v-if="showDisabledButton"
- type="button"
category="primary"
variant="success"
- class="js-disabled-merge-button"
+ data-testid="disabled-merge-button"
:disabled="true"
>
{{ s__('mrWidget|Merge') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index 7acdd695cc2..d2581f57837 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -1,6 +1,5 @@
<script>
import { GlLink, GlSprintf, GlButton } from '@gitlab/ui';
-import MrWidgetIcon from './mr_widget_icon.vue';
import Tracking from '~/tracking';
import DismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
import {
@@ -13,6 +12,7 @@ import {
SP_HELP_URL,
SP_ICON_NAME,
} from '../constants';
+import MrWidgetIcon from './mr_widget_icon.vue';
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 20ac8f5a467..428641a1109 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -2,14 +2,15 @@
import { GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as Flash } from '../../../flash';
-import statusIcon from '../mr_widget_status_icon.vue';
-import MrWidgetAuthor from '../mr_widget_author.vue';
-import eventHub from '../../event_hub';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
-import { __ } from '~/locale';
+import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+import MrWidgetAuthor from '../mr_widget_author.vue';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetAutoMergeEnabled',
@@ -53,7 +54,11 @@ export default {
},
computed: {
loading() {
- return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading;
+ return (
+ this.glFeatures.mergeRequestWidgetGraphql &&
+ this.$apollo.queries.state.loading &&
+ Object.keys(this.state).length === 0
+ );
},
mergeUser() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
@@ -78,7 +83,7 @@ export default {
canRemoveSourceBranch() {
const { currentUserId } = this.mr;
const mergeUserId = this.glFeatures.mergeRequestWidgetGraphql
- ? this.state.mergeUser?.id
+ ? getIdFromGraphQLId(this.state.mergeUser?.id)
: this.mr.mergeUserId;
const canRemoveSourceBranch = this.glFeatures.mergeRequestWidgetGraphql
? this.state.userPermissions.removeSourceBranch
@@ -96,7 +101,11 @@ export default {
.cancelAutomaticMerge()
.then((res) => res.data)
.then((data) => {
- eventHub.$emit('UpdateWidgetData', data);
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ } else {
+ eventHub.$emit('UpdateWidgetData', data);
+ }
})
.catch(() => {
this.isCancellingAutoMerge = false;
@@ -119,6 +128,11 @@ export default {
eventHub.$emit('MRWidgetUpdateRequested');
}
})
+ .then(() => {
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ this.$apollo.queries.state.refetch();
+ }
+ })
.catch(() => {
this.isRemovingSourceBranch = false;
Flash(__('Something went wrong. Please try again.'));
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 a2771bc4bfb..5f8630bf7b3 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
@@ -2,9 +2,9 @@
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
-import statusIcon from '../mr_widget_status_icon.vue';
-import autoMergeFailedQuery from '../../queries/states/auto_merge_failed.query.graphql';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+import autoMergeFailedQuery from '../../queries/states/auto_merge_failed.query.graphql';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetAutoMergeFailed',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 3d5daa4979b..2335e2984e4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -1,14 +1,14 @@
<script>
+import { GlButton, GlModalDirective, GlSkeletonLoader } from '@gitlab/ui';
import $ from 'jquery';
import { escape } from 'lodash';
-import { GlButton, GlModalDirective, GlSkeletonLoader } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
-import StatusIcon from '../mr_widget_status_icon.vue';
import userPermissionsQuery from '../../queries/permissions.query.graphql';
import conflictsStateQuery from '../../queries/states/conflicts.query.graphql';
+import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetConflicts',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index a5ec095b8ec..e973a2350a3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -1,9 +1,9 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { n__ } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
-import statusIcon from '../mr_widget_status_icon.vue';
+import { sprintf, s__, n__ } from '~/locale';
import eventHub from '../../event_hub';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetFailedToMerge',
@@ -31,7 +31,15 @@ export default {
computed: {
mergeError() {
- return this.mr.mergeError ? stripHtml(this.mr.mergeError, ' ').trim() : '';
+ const mergeError = this.mr.mergeError ? stripHtml(this.mr.mergeError, ' ').trim() : '';
+
+ return sprintf(
+ s__('mrWidget|%{mergeError}.'),
+ {
+ mergeError,
+ },
+ false,
+ );
},
timerText() {
return n__(
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 9d646dbfb3e..043d14e32a2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -3,10 +3,12 @@
import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { s__, __ } from '~/locale';
+import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
+import modalEventHub from '~/projects/commit/event_hub';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import eventHub from '../../event_hub';
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
-import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMerged',
@@ -77,6 +79,9 @@ export default {
return s__('mrWidget|Cherry-pick');
},
},
+ mounted() {
+ document.dispatchEvent(new CustomEvent('merged:UpdateActions'));
+ },
methods: {
removeSourceBranch() {
this.isMakingRequest = true;
@@ -98,6 +103,12 @@ export default {
Flash(__('Something went wrong. Please try again.'));
});
},
+ openRevertModal() {
+ modalEventHub.$emit(OPEN_REVERT_MODAL);
+ },
+ openCherryPickModal() {
+ modalEventHub.$emit(OPEN_CHERRY_PICK_MODAL);
+ },
},
};
</script>
@@ -119,9 +130,7 @@ export default {
size="small"
category="secondary"
variant="warning"
- href="#modal-revert-commit"
- data-toggle="modal"
- data-container="body"
+ @click="openRevertModal"
>
{{ revertLabel }}
</gl-button>
@@ -142,9 +151,7 @@ export default {
v-gl-tooltip.hover
:title="cherryPickTitle"
size="small"
- href="#modal-cherry-pick-commit"
- data-toggle="modal"
- data-container="body"
+ @click="openCherryPickModal"
>
{{ cherryPickLabel }}
</gl-button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
index 3f68979bc0e..f91350d4a82 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
@@ -1,10 +1,10 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
-import statusIcon from '../mr_widget_status_icon.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import missingBranchQuery from '../../queries/states/missing_branch.query.graphql';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetMissingBranch',
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 bf86e0d8b07..d15794c71b1 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
@@ -4,12 +4,12 @@ import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { deprecatedCreateFlash as Flash } from '../../../flash';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
-import statusIcon from '../mr_widget_status_icon.vue';
-import rebaseQuery from '../../queries/states/ready_to_merge.query.graphql';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
-import { deprecatedCreateFlash as Flash } from '../../../flash';
+import rebaseQuery from '../../queries/states/rebase.query.graphql';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetRebase',
@@ -159,7 +159,7 @@ export default {
<div class="rebase-state-find-class-convention media media-body space-children">
<span
v-if="rebaseInProgress || isMakingRequest"
- class="gl-font-weight-bold gl-ml-0!"
+ class="gl-font-weight-bold"
data-testid="rebase-message"
>{{ __('Rebase in progress') }}</span
>
@@ -181,17 +181,12 @@ export default {
>
{{ __('Rebase') }}
</gl-button>
- <span
- v-if="!rebasingError"
- class="gl-font-weight-bold gl-ml-0!"
- data-testid="rebase-message"
- >{{
- __(
- 'Fast-forward merge is not possible. Rebase the source branch onto the target branch.',
- )
- }}</span
- >
- <span v-else class="gl-font-weight-bold danger gl-ml-0!" data-testid="rebase-message">{{
+ <span v-if="!rebasingError" class="gl-font-weight-bold" data-testid="rebase-message">{{
+ __(
+ 'Fast-forward merge is not possible. Rebase the source branch onto the target branch.',
+ )
+ }}</span>
+ <span v-else class="gl-font-weight-bold danger" data-testid="rebase-message">{{
rebasingError
}}</span>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 14a29483d3c..f0259a975db 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -1,9 +1,13 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlButton } from '@gitlab/ui';
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
export default {
name: 'MRWidgetNothingToMerge',
+ components: {
+ GlButton,
+ },
props: {
mr: {
type: Object,
@@ -25,11 +29,13 @@ export default {
<span v-html="emptyStateSVG"></span>
</div>
<div class="text col-md-7 order-md-first col-12">
- <span>{{
- s__(
- 'mrWidgetNothingToMerge|Merge requests are a place to propose changes you have made to a project and discuss those changes with others.',
- )
- }}</span>
+ <p class="highlight">
+ {{
+ s__(
+ 'mrWidgetNothingToMerge|Merge requests are a place to propose changes you have made to a project and discuss those changes with others.',
+ )
+ }}
+ </p>
<p>
{{
s__(
@@ -45,9 +51,14 @@ export default {
}}
</p>
<div>
- <a v-if="mr.newBlobPath" :href="mr.newBlobPath" class="btn btn-inverted btn-success">{{
- __('Create file')
- }}</a>
+ <gl-button
+ v-if="mr.newBlobPath"
+ :href="mr.newBlobPath"
+ category="secondary"
+ variant="success"
+ >
+ {{ __('Create file') }}
+ </gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
index 5f56157cb89..b5d2f91c637 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -1,11 +1,26 @@
<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'PipelineFailed',
components: {
+ GlLink,
+ GlSprintf,
statusIcon,
},
+ computed: {
+ troubleshootingDocsPath() {
+ return helpPagePath('ci/troubleshooting', { anchor: 'merge-request-status-messages' });
+ },
+ },
+ i18n: {
+ failedMessage: s__(
+ `mrWidget|The pipeline for this merge request did not complete. Push a new commit to fix the failure, or check the %{linkStart}troubleshooting documentation%{linkEnd} to see other possible actions.`,
+ ),
+ },
};
</script>
@@ -14,10 +29,13 @@ export default {
<status-icon :show-disabled-button="true" status="warning" />
<div class="media-body space-children">
<span class="bold">
- {{
- s__(`mrWidget|The pipeline for this merge request failed.
-Please retry the job or push a new commit to fix the failure`)
- }}
+ <gl-sprintf :message="$options.i18n.failedMessage">
+ <template #link="{ content }">
+ <gl-link :href="troubleshootingDocsPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
</span>
</div>
</div>
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 a890b176df0..690b6e9c462 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
@@ -1,5 +1,4 @@
<script>
-import { isEmpty } from 'lodash';
import {
GlIcon,
GlButton,
@@ -11,23 +10,24 @@ import {
GlTooltipDirective,
GlSkeletonLoader,
} from '@gitlab/ui';
+import { isEmpty } from 'lodash';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
+import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import simplePoll from '~/lib/utils/simple_poll';
import { __ } from '~/locale';
-import MergeRequest from '../../../merge_request';
-import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import { deprecatedCreateFlash as Flash } from '../../../flash';
+import MergeRequest from '../../../merge_request';
+import { AUTO_MERGE_STRATEGIES, DANGER, INFO, WARNING } from '../../constants';
+import eventHub from '../../event_hub';
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import MergeRequestStore from '../../stores/mr_widget_store';
import statusIcon from '../mr_widget_status_icon.vue';
-import eventHub from '../../event_hub';
-import SquashBeforeMerge from './squash_before_merge.vue';
-import CommitsHeader from './commits_header.vue';
import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue';
-import { AUTO_MERGE_STRATEGIES, DANGER, INFO, WARNING } from '../../constants';
+import CommitsHeader from './commits_header.vue';
+import SquashBeforeMerge from './squash_before_merge.vue';
const PIPELINE_RUNNING_STATE = 'running';
const PIPELINE_FAILED_STATE = 'failed';
@@ -53,10 +53,13 @@ export default {
result({ data }) {
this.state = {
...data.project.mergeRequest,
- mergeRequestsFfOnlyEnabled: data.mergeRequestsFfOnlyEnabled,
- onlyAllowMergeIfPipelineSucceeds: data.onlyAllowMergeIfPipelineSucceeds,
+ mergeRequestsFfOnlyEnabled: data.project.mergeRequestsFfOnlyEnabled,
+ onlyAllowMergeIfPipelineSucceeds: data.project.onlyAllowMergeIfPipelineSucceeds,
};
- this.removeSourceBranch = data.project.mergeRequest.shouldRemoveSourceBranch;
+ this.removeSourceBranch =
+ data.project.mergeRequest.shouldRemoveSourceBranch ||
+ data.project.mergeRequest.forceRemoveSourceBranch ||
+ false;
this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage;
this.squashBeforeMerge = data.project.mergeRequest.squashOnMerge;
this.isSquashReadOnly = data.project.squashReadOnly;
@@ -277,7 +280,20 @@ export default {
return this.mr.mergeRequestDiffsPath;
},
},
+ mounted() {
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
+ }
+ },
+ beforeDestroy() {
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
+ }
+ },
methods: {
+ updateGraphqlState() {
+ return this.$apollo.queries.state.refetch();
+ },
updateMergeCommitMessage(includeDescription) {
const commitMessage = this.glFeatures.mergeRequestWidgetGraphql
? this.state.defaultMergeCommitMessage
@@ -326,6 +342,10 @@ export default {
} else if (hasError) {
eventHub.$emit('FailedToMerge', data.merge_error);
}
+
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ this.updateGraphqlState();
+ }
})
.catch(() => {
this.isMakingRequest = false;
@@ -442,6 +462,7 @@ export default {
:variant="mergeButtonVariant"
:disabled="isMergeButtonDisabled"
:loading="isMakingRequest"
+ data-qa-selector="merge_button"
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
@@ -532,7 +553,7 @@ export default {
</div>
<merge-train-helper-text
v-if="shouldRenderMergeTrainHelperText"
- :pipeline-id="pipeline.id"
+ :pipeline-id="pipelineId"
:pipeline-link="pipeline.path"
:merge-train-length="stateData.mergeTrainsCount"
:merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index 78e69b9ff9b..329964d009a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
-import statusIcon from '../mr_widget_status_icon.vue';
import notesEventHub from '~/notes/event_hub';
+import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'UnresolvedDiscussions',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 3f1db815f95..af305815381 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -1,17 +1,17 @@
<script>
-import $ from 'jquery';
import { GlButton } from '@gitlab/ui';
import { produce } from 'immer';
-import { __ } from '~/locale';
+import $ from 'jquery';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { __ } from '~/locale';
import MergeRequest from '~/merge_request';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import getStateQuery from '../../queries/get_state.query.graphql';
import workInProgressQuery from '../../queries/states/work_in_progress.query.graphql';
import removeWipMutation from '../../queries/toggle_wip.mutation.graphql';
import StatusIcon from '../mr_widget_status_icon.vue';
-import eventHub from '../../event_hub';
export default {
name: 'WorkInProgress',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
index 180db7828a8..25f339b362f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
@@ -1,9 +1,9 @@
<script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
-import { n__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
-import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
import Poll from '~/lib/utils/poll';
+import { n__ } from '~/locale';
+import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
import TerraformPlan from './terraform_plan.vue';
export default {
@@ -128,12 +128,9 @@ export default {
</template>
<template #content>
- <terraform-plan
- v-for="(plan, key) in plansObject"
- :key="key"
- :plan="plan"
- class="mr-widget-body"
- />
+ <div class="mr-widget-body gl-pb-1">
+ <terraform-plan v-for="(plan, key) in plansObject" :key="key" :plan="plan" />
+ </div>
</template>
</mr-widget-expanable-section>
</section>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
index b74e036d9d9..25e3dae92e8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
@@ -15,6 +15,16 @@ export default {
type: Object,
},
},
+ i18n: {
+ changes: s__(
+ 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
+ ),
+ generationErrored: s__('Terraform|Generating the report caused an error.'),
+ namedReportFailed: s__('Terraform|The report %{name} failed to generate.'),
+ namedReportGenerated: s__('Terraform|The report %{name} was generated in your pipelines.'),
+ reportFailed: s__('Terraform|A report failed to generate.'),
+ reportGenerated: s__('Terraform|A report was generated in your pipelines.'),
+ },
computed: {
addNum() {
return Number(this.plan.create);
@@ -30,23 +40,21 @@ export default {
},
reportChangeText() {
if (this.validPlanValues) {
- return s__(
- 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
- );
+ return this.$options.i18n.changes;
}
- return s__('Terraform|Generating the report caused an error.');
+ return this.$options.i18n.generationErrored;
},
reportHeaderText() {
if (this.validPlanValues) {
return this.plan.job_name
- ? s__('Terraform|The Terraform report %{name} was generated in your pipelines.')
- : s__('Terraform|A Terraform report was generated in your pipelines.');
+ ? this.$options.i18n.namedReportGenerated
+ : this.$options.i18n.reportGenerated;
}
return this.plan.job_name
- ? s__('Terraform|The Terraform report %{name} failed to generate.')
- : s__('Terraform|A Terraform report failed to generate.');
+ ? this.$options.i18n.namedReportFailed
+ : this.$options.i18n.reportFailed;
},
validPlanValues() {
return this.addNum + this.changeNum + this.deleteNum >= 0;
@@ -56,16 +64,16 @@ export default {
</script>
<template>
- <div class="gl-display-flex">
+ <div class="gl-display-flex gl-pb-3">
<span
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-mr-3 gl-align-self-start gl-mt-1"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-align-self-start gl-px-2"
>
- <gl-icon :name="iconType" :size="18" data-testid="change-type-icon" />
+ <gl-icon :name="iconType" :size="16" data-testid="change-type-icon" />
</span>
- <div class="gl-display-flex gl-flex-fill-1 gl-flex-direction-column flex-md-row">
- <div class="gl-flex-fill-1 gl-display-flex gl-flex-direction-column">
- <p class="gl-m-0 gl-pr-1">
+ <div class="gl-display-flex gl-flex-fill-1 gl-flex-direction-column flex-md-row gl-pl-3">
+ <div class="gl-flex-fill-1 gl-display-flex gl-flex-direction-column gl-pr-3">
+ <p class="gl-mb-3 gl-line-height-normal">
<gl-sprintf :message="reportHeaderText">
<template #name>
<strong>{{ plan.job_name }}</strong>
@@ -73,7 +81,7 @@ export default {
</gl-sprintf>
</p>
- <p class="gl-m-0">
+ <p class="gl-mb-3 gl-line-height-normal">
<gl-sprintf :message="reportChangeText">
<template #addNum>
<strong>{{ addNum }}</strong>
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index d512877a20d..c1c491f6fe0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,8 +1,12 @@
+// This is a false violation of @gitlab/no-runtime-template-compiler, since it
+// creates a new Vue instance by spreading a _valid_ Vue component definition
+// into the Vue constructor.
+/* eslint-disable @gitlab/no-runtime-template-compiler */
import Vue from 'vue';
-import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import VueApollo from 'vue-apollo';
-import Translate from '../vue_shared/translate';
+import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import createDefaultClient from '~/lib/graphql';
+import Translate from '../vue_shared/translate';
import { registerExtension } from './components/extensions';
import issueExtension from './extensions/issues';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index fe512d68ea2..23215982e6e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -35,5 +35,8 @@ export default {
shouldRenderMergeTrainHelperText() {
return false;
},
+ pipelineId() {
+ return this.pipeline.id;
+ },
},
};
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 519576d9fe6..2a53eb8b241 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
@@ -1,50 +1,50 @@
<script>
+import { GlSafeHtmlDirective } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
-import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
+import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
+import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
-import { GlSafeHtmlDirective } from '@gitlab/ui';
+import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
+import notify from '~/lib/utils/notify';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
-import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import { deprecatedCreateFlash as createFlash } from '../flash';
-import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
+import { setFaviconOverlay } from '../lib/utils/favicon';
+import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
+import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue';
+import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
import Loading from './components/loading.vue';
+import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
import WidgetHeader from './components/mr_widget_header.vue';
-import WidgetSuggestPipeline from './components/mr_widget_suggest_pipeline.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
-import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
-import MergedState from './components/states/mr_widget_merged.vue';
-import ClosedState from './components/states/mr_widget_closed.vue';
-import MergingState from './components/states/mr_widget_merging.vue';
-import RebaseState from './components/states/mr_widget_rebase.vue';
-import WorkInProgressState from './components/states/work_in_progress.vue';
+import WidgetSuggestPipeline from './components/mr_widget_suggest_pipeline.vue';
+import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import ArchivedState from './components/states/mr_widget_archived.vue';
+import MrWidgetAutoMergeEnabled from './components/states/mr_widget_auto_merge_enabled.vue';
+import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
+import CheckingState from './components/states/mr_widget_checking.vue';
+import ClosedState from './components/states/mr_widget_closed.vue';
import ConflictsState from './components/states/mr_widget_conflicts.vue';
-import NothingToMergeState from './components/states/nothing_to_merge.vue';
+import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
+import MergedState from './components/states/mr_widget_merged.vue';
+import MergingState from './components/states/mr_widget_merging.vue';
import MissingBranchState from './components/states/mr_widget_missing_branch.vue';
import NotAllowedState from './components/states/mr_widget_not_allowed.vue';
-import ReadyToMergeState from './components/states/ready_to_merge.vue';
-import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue';
import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue';
+import RebaseState from './components/states/mr_widget_rebase.vue';
+import NothingToMergeState from './components/states/nothing_to_merge.vue';
import PipelineFailedState from './components/states/pipeline_failed.vue';
-import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
-import MrWidgetAutoMergeEnabled from './components/states/mr_widget_auto_merge_enabled.vue';
-import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
-import CheckingState from './components/states/mr_widget_checking.vue';
+import ReadyToMergeState from './components/states/ready_to_merge.vue';
+import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue';
+import WorkInProgressState from './components/states/work_in_progress.vue';
// import ExtensionsContainer from './components/extensions/container';
-import eventHub from './event_hub';
-import notify from '~/lib/utils/notify';
-import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import TerraformPlan from './components/terraform/mr_widget_terraform_container.vue';
-import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue';
-import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
-import { setFaviconOverlay } from '../lib/utils/favicon';
-import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
+import eventHub from './event_hub';
+import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
export default {
@@ -94,7 +94,6 @@ export default {
state: {
query: getStateQuery,
manual: true,
- pollInterval: 10 * 1000,
skip() {
return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql;
},
@@ -181,7 +180,7 @@ export default {
);
},
shouldRenderSecurityReport() {
- return Boolean(window.gon?.features?.coreSecurityMrWidget && this.mr.pipeline.id);
+ return Boolean(this.mr.pipeline.id);
},
mergeError() {
let { mergeError } = this.mr;
@@ -191,7 +190,7 @@ export default {
}
return sprintf(
- s__('mrWidget|Merge failed: %{mergeError}. Please try again.'),
+ s__('mrWidget|%{mergeError}. Try again.'),
{
mergeError,
},
@@ -286,6 +285,10 @@ export default {
return new MRWidgetService(this.getServiceEndpoints(store));
},
checkStatus(cb, isRebased) {
+ if (window.gon?.features?.mergeRequestWidgetGraphql) {
+ this.$apollo.queries.state.refetch();
+ }
+
return this.service
.checkStatus()
.then(({ data }) => {
@@ -365,6 +368,7 @@ export default {
const el = document.createElement('div');
el.innerHTML = res.data;
document.body.appendChild(el);
+ document.dispatchEvent(new CustomEvent('merged:UpdateActions'));
Project.initRefSwitcher();
}
})
@@ -464,6 +468,7 @@ export default {
:head-path="mr.codeclimate.head_path"
:head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath"
+ :codequality-reports-path="mr.codequalityReportsPath"
:codequality-help-path="mr.codequalityHelpPath"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
index 44fc1cc7f23..13ea07884b1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
@@ -14,10 +14,12 @@ query getState($projectPath: ID!, $iid: String!) {
pipelines(first: 1) {
nodes {
status
+ warnings
}
}
shouldBeRebased
sourceBranchExists
+ state
targetBranchExists
userPermissions {
canMerge
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql
index 64cd70fcf42..ad715599eb1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql
@@ -1,6 +1,7 @@
fragment autoMergeEnabled on MergeRequest {
autoMergeStrategy
mergeUser {
+ id
name
username
webUrl
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
index bdcb7a8206b..daf21e75b3b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql
@@ -4,7 +4,6 @@ query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
- mergeTrainsCount
}
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
index 9479ef3cf79..8ee45b05431 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
@@ -5,6 +5,7 @@ fragment ReadyToMerge on Project {
mergeRequest(iid: $iid) {
autoMergeEnabled
shouldRemoveSourceBranch
+ forceRemoveSourceBranch
defaultMergeCommitMessage
defaultMergeCommitMessageWithDescription
defaultSquashCommitMessage
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js
index 419793977f0..5fd5950859b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js
@@ -1,7 +1,7 @@
import Visibility from 'visibilityjs';
import axios from '~/lib/utils/axios_utils';
-import Poll from '~/lib/utils/poll';
import httpStatusCodes from '~/lib/utils/http_status';
+import Poll from '~/lib/utils/poll';
import * as types from './mutation_types';
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js
index e6cb5ead089..a2edfa94a48 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
-import mutations from './mutations';
import * as getters from './getters';
+import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
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 a6bbab47a06..a0f14f558d2 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
@@ -1,9 +1,9 @@
import { format } from 'timeago.js';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import mrEventHub from '~/merge_request/eventhub';
-import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
+import { stateKey } from './state_maps';
export default class MergeRequestStore {
constructor(data) {
@@ -156,9 +156,9 @@ export default class MergeRequestStore {
this.setState();
- mrEventHub.$emit('mr.state.updated', {
- state: this.mergeRequestState,
- });
+ if (!window.gon?.features?.mergeRequestWidgetGraphql) {
+ this.emitUpdatedState();
+ }
}
setGraphqlData(project) {
@@ -172,6 +172,11 @@ export default class MergeRequestStore {
this.canBeMerged = mergeRequest.mergeStatus === 'can_be_merged';
this.canMerge = mergeRequest.userPermissions.canMerge;
this.ciStatus = pipeline?.status.toLowerCase();
+
+ if (pipeline?.warnings && this.ciStatus === 'success') {
+ this.ciStatus = `${this.ciStatus}-with-warnings`;
+ }
+
this.commitsCount = mergeRequest.commitCount || 10;
this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists;
this.hasConflicts = mergeRequest.conflicts;
@@ -182,7 +187,9 @@ export default class MergeRequestStore {
this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha;
this.shouldBeRebased = mergeRequest.shouldBeRebased;
this.workInProgress = mergeRequest.workInProgress;
+ this.mergeRequestState = mergeRequest.state;
+ this.emitUpdatedState();
this.setState();
}
@@ -208,6 +215,12 @@ export default class MergeRequestStore {
}
}
+ emitUpdatedState() {
+ mrEventHub.$emit('mr.state.updated', {
+ state: this.mergeRequestState,
+ });
+ }
+
setPaths(data) {
// Paths are set on the first load of the page and not auto-refreshed
this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path;
@@ -241,10 +254,11 @@ export default class MergeRequestStore {
this.isDismissedSuggestPipeline = data.is_dismissed_suggest_pipeline;
this.securityReportsDocsPath = data.security_reports_docs_path;
- // codeclimate
+ // code quality
const blobPath = data.blob_path || {};
this.headBlobPath = blobPath.head_path || '';
this.baseBlobPath = blobPath.base_path || '';
+ this.codequalityReportsPath = data.codequality_reports_path;
this.codequalityHelpPath = data.codequality_help_path;
this.codeclimate = data.codeclimate;
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index 895c6e76019..bcea7ca654e 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -11,25 +11,25 @@ import {
GlButton,
GlSafeHtmlDirective,
} from '@gitlab/ui';
-import * as Sentry from '~/sentry/wrapper';
-import { s__ } from '~/locale';
-import alertQuery from '../graphql/queries/details.query.graphql';
-import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
-import { fetchPolicies } from '~/lib/graphql';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
-import initUserPopovers from '~/user_popovers';
-import { ALERTS_SEVERITY_LABELS, trackAlertsDetailsViewsOptions } from '../constants';
-import createIssueMutation from '../graphql/mutations/create_issue_from_alert.mutation.graphql';
-import toggleSidebarStatusMutation from '../graphql/mutations/toggle_sidebar_status.mutation.graphql';
+import { fetchPolicies } from '~/lib/graphql';
+import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
+import * as Sentry from '~/sentry/wrapper';
import Tracking from '~/tracking';
-import { toggleContainerClasses } from '~/lib/utils/dom_utils';
-import SystemNote from './system_notes/system_note.vue';
-import AlertSidebar from './alert_sidebar.vue';
-import AlertMetrics from './alert_metrics.vue';
+import initUserPopovers from '~/user_popovers';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { SEVERITY_LEVELS } from '../constants';
+import createIssueMutation from '../graphql/mutations/alert_issue_create.mutation.graphql';
+import toggleSidebarStatusMutation from '../graphql/mutations/alert_sidebar_status.mutation.graphql';
+import alertQuery from '../graphql/queries/alert_details.query.graphql';
+import sidebarStatusQuery from '../graphql/queries/alert_sidebar_status.query.graphql';
+import AlertMetrics from './alert_metrics.vue';
+import AlertSidebar from './alert_sidebar.vue';
import AlertSummaryRow from './alert_summary_row.vue';
+import SystemNote from './system_notes/system_note.vue';
const containerEl = document.querySelector('.page-with-contextual-sidebar');
@@ -44,7 +44,7 @@ export default {
directives: {
SafeHtml: GlSafeHtmlDirective,
},
- severityLabels: ALERTS_SEVERITY_LABELS,
+ severityLabels: SEVERITY_LEVELS,
tabsConfig: [
{
id: 'overview',
@@ -83,12 +83,18 @@ export default {
alertId: {
default: '',
},
+ isThreatMonitoringPage: {
+ default: false,
+ },
projectId: {
default: '',
},
projectIssuesPath: {
default: '',
},
+ trackAlertsDetailsViewsOptions: {
+ default: null,
+ },
},
apollo: {
alert: {
@@ -155,7 +161,9 @@ export default {
},
},
mounted() {
- this.trackPageViews();
+ if (this.trackAlertsDetailsViewsOptions) {
+ this.trackPageViews();
+ }
toggleContainerClasses(containerEl, {
'issuable-bulk-update-sidebar': true,
'right-sidebar-expanded': true,
@@ -217,7 +225,7 @@ export default {
return joinPaths(this.projectIssuesPath, issueId);
},
trackPageViews() {
- const { category, action } = trackAlertsDetailsViewsOptions;
+ const { category, action } = this.trackAlertsDetailsViewsOptions;
Tracking.event(category, action);
},
},
@@ -359,7 +367,11 @@ export default {
</alert-summary-row>
<alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
- <gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
+ <gl-tab
+ v-if="isThreatMonitoringPage"
+ :data-testid="$options.tabsConfig[1].id"
+ :title="$options.tabsConfig[1].title"
+ >
<alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
</gl-tab>
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
diff --git a/app/assets/javascripts/alert_management/components/alert_metrics.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_metrics.vue
index dd4faa03c00..dd4faa03c00 100644
--- a/app/assets/javascripts/alert_management/components/alert_metrics.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_metrics.vue
diff --git a/app/assets/javascripts/alert_management/components/alert_sidebar.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
index 41d77716592..a01bd462196 100644
--- a/app/assets/javascripts/alert_management/components/alert_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
@@ -1,10 +1,9 @@
<script>
+import sidebarStatusQuery from '../graphql/queries/alert_sidebar_status.query.graphql';
+import SidebarAssignees from './sidebar/sidebar_assignees.vue';
import SidebarHeader from './sidebar/sidebar_header.vue';
-import SidebarTodo from './sidebar/sidebar_todo.vue';
import SidebarStatus from './sidebar/sidebar_status.vue';
-import SidebarAssignees from './sidebar/sidebar_assignees.vue';
-
-import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
+import SidebarTodo from './sidebar/sidebar_todo.vue';
export default {
components: {
@@ -20,6 +19,10 @@ export default {
projectId: {
default: '',
},
+ // TODO remove this limitation in https://gitlab.com/gitlab-org/gitlab/-/issues/296717
+ isThreatMonitoringPage: {
+ default: false,
+ },
},
props: {
alert: {
@@ -63,6 +66,7 @@ export default {
@alert-error="$emit('alert-error', $event)"
/>
<sidebar-status
+ v-if="!isThreatMonitoringPage"
:project-path="projectPath"
:alert="alert"
@toggle-sidebar="$emit('toggle-sidebar')"
diff --git a/app/assets/javascripts/alert_management/components/alert_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
index 2afdeb8b6fd..8d5eb24ed1d 100644
--- a/app/assets/javascripts/alert_management/components/alert_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
@@ -1,9 +1,8 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
-import { trackAlertStatusUpdateOptions } from '../constants';
-import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql';
export default {
i18n: {
@@ -21,6 +20,11 @@ export default {
GlDropdown,
GlDropdownItem,
},
+ inject: {
+ trackAlertStatusUpdateOptions: {
+ default: null,
+ },
+ },
props: {
projectPath: {
type: String,
@@ -58,7 +62,9 @@ export default {
},
})
.then((resp) => {
- this.trackStatusUpdate(status);
+ if (this.trackAlertStatusUpdateOptions) {
+ this.trackStatusUpdate(status);
+ }
const errors = resp.data?.updateAlertStatus?.errors || [];
if (errors[0]) {
@@ -81,7 +87,7 @@ export default {
});
},
trackStatusUpdate(status) {
- const { category, action, label } = trackAlertStatusUpdateOptions;
+ const { category, action, label } = this.trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
},
diff --git a/app/assets/javascripts/alert_management/components/alert_summary_row.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_summary_row.vue
index 13835b7e2fa..13835b7e2fa 100644
--- a/app/assets/javascripts/alert_management/components/alert_summary_row.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_summary_row.vue
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue
index c39a72a45b9..c39a72a45b9 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index 2a999b908f9..2a999b908f9 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
index 70902a204f8..fd40b5d9f65 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
@@ -27,7 +27,7 @@ export default {
<template>
<div class="block gl-display-flex gl-justify-content-space-between">
<span class="issuable-header-text hide-collapsed">
- {{ __('To-Do') }}
+ {{ __('To Do') }}
</span>
<sidebar-todo
v-if="!sidebarCollapsed"
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
index 0a2bad5510b..0a2bad5510b 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
index 485395bcac2..39ac6c7feca 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
@@ -1,15 +1,15 @@
<script>
import produce from 'immer';
+import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
import { s__ } from '~/locale';
import Todo from '~/sidebar/components/todo_toggle/todo.vue';
import createAlertTodoMutation from '../../graphql/mutations/alert_todo_create.mutation.graphql';
-import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
-import alertQuery from '../../graphql/queries/details.query.graphql';
+import alertQuery from '../../graphql/queries/alert_details.query.graphql';
export default {
i18n: {
UPDATE_ALERT_TODO_ERROR: s__(
- 'AlertManagement|There was an error while updating the To-Do of the alert.',
+ 'AlertManagement|There was an error while updating the to-do item of the alert.',
),
},
components: {
diff --git a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
index 3705e36a579..3705e36a579 100644
--- a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
diff --git a/app/assets/javascripts/vue_shared/alert_details/constants.js b/app/assets/javascripts/vue_shared/alert_details/constants.js
new file mode 100644
index 00000000000..2ab5160534c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/alert_details/constants.js
@@ -0,0 +1,31 @@
+import { s__ } from '~/locale';
+
+export const SEVERITY_LEVELS = {
+ CRITICAL: s__('severity|Critical'),
+ HIGH: s__('severity|High'),
+ MEDIUM: s__('severity|Medium'),
+ LOW: s__('severity|Low'),
+ INFO: s__('severity|Info'),
+ UNKNOWN: s__('severity|Unknown'),
+};
+
+/* eslint-disable @gitlab/require-i18n-strings */
+export const PAGE_CONFIG = {
+ OPERATIONS: {
+ TITLE: 'OPERATIONS',
+ // Tracks snowplow event when user views alert details
+ TRACK_ALERTS_DETAILS_VIEWS_OPTIONS: {
+ category: 'Alert Management',
+ action: 'view_alert_details',
+ },
+ // Tracks snowplow event when alert status is updated
+ TRACK_ALERT_STATUS_UPDATE_OPTIONS: {
+ category: 'Alert Management',
+ action: 'update_alert_status',
+ label: 'Status',
+ },
+ },
+ THREAT_MONITORING: {
+ TITLE: 'THREAT_MONITORING',
+ },
+};
diff --git a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/fragments/alert_detail_item.fragment.graphql
index 9a9ae369519..9a9ae369519 100644
--- a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/fragments/alert_detail_item.fragment.graphql
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
index bc4d91a51d1..bc4d91a51d1 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
index 63d952a4857..63d952a4857 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_sidebar_status.mutation.graphql
index f666fcd6782..f666fcd6782 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_sidebar_status.mutation.graphql
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql
index ac9858c104f..dc961b5eb90 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql
@@ -1,4 +1,4 @@
-#import "../fragments/detail_item.fragment.graphql"
+#import "../fragments/alert_detail_item.fragment.graphql"
mutation alertTodoCreate($projectPath: ID!, $iid: String!) {
alertTodoCreate(input: { iid: $iid, projectPath: $projectPath }) {
diff --git a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_details.query.graphql
index 8881f49b689..5ee2cf7ca44 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_details.query.graphql
@@ -1,4 +1,4 @@
-#import "../fragments/detail_item.fragment.graphql"
+#import "../fragments/alert_detail_item.fragment.graphql"
query alertDetails($fullPath: ID!, $alertId: String) {
project(fullPath: $fullPath) {
diff --git a/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_status.query.graphql
index 61c570c5cd0..61c570c5cd0 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/queries/alert_sidebar_status.query.graphql
diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/vue_shared/alert_details/index.js
index 4217b702d0a..3ea43d7a843 100644
--- a/app/assets/javascripts/alert_management/details.js
+++ b/app/assets/javascripts/vue_shared/alert_details/index.js
@@ -4,14 +4,15 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import AlertDetails from './components/alert_details.vue';
-import sidebarStatusQuery from './graphql/queries/sidebar_status.query.graphql';
+import { PAGE_CONFIG } from './constants';
+import sidebarStatusQuery from './graphql/queries/alert_sidebar_status.query.graphql';
import createRouter from './router';
Vue.use(VueApollo);
export default (selector) => {
const domEl = document.querySelector(selector);
- const { alertId, projectPath, projectIssuesPath, projectId } = domEl.dataset;
+ const { alertId, projectPath, projectIssuesPath, projectId, page } = domEl.dataset;
const router = createRouter();
const resolvers = {
@@ -48,18 +49,31 @@ export default (selector) => {
},
});
+ const provide = {
+ projectPath,
+ alertId,
+ page,
+ projectIssuesPath,
+ projectId,
+ };
+
+ if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
+ const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
+ page
+ ];
+ provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
+ provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
+ } else if (page === PAGE_CONFIG.THREAT_MONITORING.TITLE) {
+ provide.isThreatMonitoringPage = true;
+ }
+
// eslint-disable-next-line no-new
new Vue({
el: selector,
components: {
AlertDetails,
},
- provide: {
- projectPath,
- alertId,
- projectIssuesPath,
- projectId,
- },
+ provide,
apolloProvider,
router,
render(createElement) {
diff --git a/app/assets/javascripts/alert_management/router.js b/app/assets/javascripts/vue_shared/alert_details/router.js
index 5687fe4e0f5..5687fe4e0f5 100644
--- a/app/assets/javascripts/alert_management/router.js
+++ b/app/assets/javascripts/vue_shared/alert_details/router.js
diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue
index 2dc2c27f7ea..13472b48e84 100644
--- a/app/assets/javascripts/vue_shared/components/actions_button.vue
+++ b/app/assets/javascripts/vue_shared/components/actions_button.vue
@@ -76,14 +76,13 @@ export default {
<template v-for="(action, index) in actions">
<gl-dropdown-item
:key="action.key"
- class="gl-dropdown-item-deprecated-adapter"
:is-check-item="true"
:is-checked="action.key === selectedAction.key"
:secondary-text="action.secondaryText"
:data-testid="`action_${action.key}`"
@click="handleItemClick(action)"
>
- {{ action.text }}
+ <span class="gl-font-weight-bold">{{ action.text }}</span>
</gl-dropdown-item>
<gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
index 655b867574d..3d49a1cb1c5 100644
--- a/app/assets/javascripts/vue_shared/components/alert_details_table.vue
+++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
@@ -1,12 +1,12 @@
<script>
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { reduce } from 'lodash';
-import { s__ } from '~/locale';
import {
capitalizeFirstCharacter,
convertToSentenceCase,
splitCamelCase,
} from '~/lib/utils/text_utility';
+import { s__ } from '~/locale';
const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!';
const tdClass = 'gl-border-gray-100! gl-p-5!';
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index c1da2b8c305..ce67d33d4a1 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -1,9 +1,9 @@
<script>
/* eslint-disable vue/no-v-html */
-import { groupBy } from 'lodash';
import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { glEmojiTag } from '../../emoji';
+import { groupBy } from 'lodash';
import { __, sprintf } from '~/locale';
+import { glEmojiTag } from '../../emoji';
// Internal constant, specific to this component, used when no `currentUserId` is given
const NO_USER_ID = -1;
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
index 6f7723955bf..db61d0f6b05 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
@@ -1,5 +1,5 @@
-import { SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
import eventHub from '~/blob/components/eventhub';
+import { SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
export default {
props: {
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index d0f5570db6b..a8a053c0d9e 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -1,8 +1,8 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { handleBlobRichViewer } from '~/blob/viewer';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import ViewerMixin from './mixins';
-import { handleBlobRichViewer } from '~/blob/viewer';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index 646e1703f1e..5bb31f55e6c 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
-import ViewerMixin from './mixins';
import { HIGHLIGHT_CLASS_NAME } from './constants';
+import ViewerMixin from './mixins';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
index 5c6bd5892ae..cd5f63afc79 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
@@ -6,8 +6,8 @@ import {
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
import { getHTTPProtocol } from '~/lib/utils/url_utility';
+import { __, sprintf } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
index 6977692e30c..0ff33e462b4 100644
--- a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
@@ -3,12 +3,16 @@
* Renders a color picker input with preset colors to choose from
*
* @example
- * <color-picker :label="__('Background color')" set-color="#FF0000" />
+ * <color-picker
+ :invalid-feedback="__('Please enter a valid hex (#RRGGBB or #RGB) color value')"
+ :label="__('Background color')"
+ :value="#FF0000"
+ state="isValidColor"
+ />
*/
import { GlFormGroup, GlFormInput, GlFormInputGroup, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
-const VALID_RGB_HEX_COLOR = /^#([0-9A-F]{3}){1,2}$/i;
const PREVIEW_COLOR_DEFAULT_CLASSES =
'gl-relative gl-w-7 gl-bg-gray-10 gl-rounded-top-left-base gl-rounded-bottom-left-base';
@@ -24,21 +28,26 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
+ invalidFeedback: {
+ type: String,
+ required: false,
+ default: __('Please enter a valid hex (#RRGGBB or #RGB) color value'),
+ },
label: {
type: String,
required: false,
default: '',
},
- setColor: {
+ value: {
type: String,
required: false,
default: '',
},
- },
- data() {
- return {
- selectedColor: this.setColor.trim() || '',
- };
+ state: {
+ type: Boolean,
+ required: false,
+ default: null,
+ },
},
computed: {
description() {
@@ -50,46 +59,30 @@ export default {
return gon.suggested_label_colors;
},
previewColor() {
- if (this.isValidColor) {
- return { backgroundColor: this.selectedColor };
+ if (this.state) {
+ return { backgroundColor: this.value };
}
return {};
},
previewColorClasses() {
- const borderStyle = this.isInvalidColor
- ? 'gl-inset-border-1-red-500'
- : 'gl-inset-border-1-gray-400';
+ const borderStyle =
+ this.state === false ? 'gl-inset-border-1-red-500' : 'gl-inset-border-1-gray-400';
return `${PREVIEW_COLOR_DEFAULT_CLASSES} ${borderStyle}`;
},
hasSuggestedColors() {
return Object.keys(this.suggestedColors).length;
},
- isInvalidColor() {
- return this.isValidColor === false;
- },
- isValidColor() {
- if (this.selectedColor === '') {
- return null;
- }
-
- return VALID_RGB_HEX_COLOR.test(this.selectedColor);
- },
},
methods: {
handleColorChange(color) {
- this.selectedColor = color.trim();
-
- if (this.isValidColor) {
- this.$emit('input', this.selectedColor);
- }
+ this.$emit('input', color.trim());
},
},
i18n: {
fullDescription: __('Choose any color. Or you can choose one of the suggested colors below'),
shortDescription: __('Choose any color'),
- invalid: __('Please enter a valid hex (#RRGGBB or #RGB) color value'),
},
};
</script>
@@ -100,17 +93,17 @@ export default {
:label="label"
label-for="color-picker"
:description="description"
- :invalid-feedback="this.$options.i18n.invalid"
- :state="isValidColor"
+ :invalid-feedback="invalidFeedback"
+ :state="state"
:class="{ 'gl-mb-3!': hasSuggestedColors }"
>
<gl-form-input-group
id="color-picker"
- :state="isValidColor"
max-length="7"
type="text"
class="gl-align-center gl-rounded-0 gl-rounded-top-right-base gl-rounded-bottom-right-base"
- :value="selectedColor"
+ :value="value"
+ :state="state"
@input="handleColorChange"
>
<template #prepend>
@@ -119,7 +112,7 @@ export default {
type="color"
class="gl-absolute gl-top-0 gl-left-0 gl-h-full! gl-p-0! gl-m-0! gl-cursor-pointer gl-opacity-0"
tabindex="-1"
- :value="selectedColor"
+ :value="value"
@input="handleColorChange"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index deca934e283..d1eee62683b 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,6 +1,6 @@
<script>
-import { isString, isEmpty } from 'lodash';
import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
+import { isString, isEmpty } from 'lodash';
import { __, sprintf } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
@@ -133,6 +133,9 @@ export default {
? sprintf(__("%{username}'s avatar"), { username: this.author.username })
: null;
},
+ refUrl() {
+ return this.commitRef.ref_url || this.commitRef.path;
+ },
},
};
</script>
@@ -156,9 +159,10 @@ export default {
<gl-link
v-else
v-gl-tooltip
- :href="commitRef.ref_url"
+ :href="refUrl"
:title="commitRef.name"
class="ref-name"
+ data-testid="ref-name"
>{{ commitRef.name }}</gl-link
>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
index 9ff35132ac9..1370f7b2a8c 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
@@ -1,7 +1,7 @@
<script>
-import MarkdownViewer from './viewers/markdown_viewer.vue';
-import ImageViewer from './viewers/image_viewer.vue';
import DownloadViewer from './viewers/download_viewer.vue';
+import ImageViewer from './viewers/image_viewer.vue';
+import MarkdownViewer from './viewers/markdown_viewer.vue';
export default {
components: {
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 9ece6a52805..a49eb7fd611 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
@@ -1,6 +1,7 @@
<script>
import { throttle } from 'lodash';
-import { numberToHumanSize } from '../../../../lib/utils/number_utils';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { encodeSaferUrl } from '~/lib/utils/url_utility';
export default {
props: {
@@ -43,6 +44,9 @@ export default {
hasDimensions() {
return this.width && this.height;
},
+ safePath() {
+ return encodeSaferUrl(this.path);
+ },
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
@@ -84,7 +88,7 @@ export default {
<template>
<div data-testid="image-viewer" data-qa-selector="image_viewer_container">
<div :class="innerCssClasses" class="position-relative">
- <img ref="contentImg" :src="path" @load="onImgLoad" />
+ <img ref="contentImg" :src="safePath" @load="onImgLoad" />
<slot
name="image-overlay"
:rendered-width="renderedWidth"
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 24386c90954..3790a509f26 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,9 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { forEach, escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
index d0c2672b162..1a96cabf755 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
@@ -1,8 +1,7 @@
<script>
import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
+import { __, sprintf } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import DateTimePickerInput from './date_time_picker_input.vue';
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
index 39c1caf928e..190d4e1f104 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
@@ -1,6 +1,6 @@
<script>
-import { uniqueId } from 'lodash';
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
import { __, sprintf } from '~/locale';
import { dateFormats } from './date_time_picker_lib';
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 e755494a668..7080e046b30 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
@@ -1,9 +1,9 @@
<script>
import { diffViewerModes, diffModes } from '~/ide/constants';
-import ImageDiffViewer from './viewers/image_diff_viewer.vue';
import DownloadDiffViewer from './viewers/download_diff_viewer.vue';
-import RenamedFile from './viewers/renamed.vue';
+import ImageDiffViewer from './viewers/image_diff_viewer.vue';
import ModeChanged from './viewers/mode_changed.vue';
+import RenamedFile from './viewers/renamed.vue';
export default {
props: {
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 433aafdeb9e..a3d9b0ace34 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
@@ -1,6 +1,6 @@
<script>
-import { pixeliseValue } from '../../../lib/utils/dom_utils';
import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
+import { pixeliseValue } from '../../../lib/utils/dom_utils';
export default {
components: {
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 acca6ba117f..41acc63cea9 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
@@ -1,7 +1,7 @@
<script>
import { throttle } from 'lodash';
-import { pixeliseValue } from '../../../lib/utils/dom_utils';
import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue';
+import { pixeliseValue } from '../../../lib/utils/dom_utils';
export default {
components: {
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 00033145603..f5ac65bcd1f 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
@@ -1,9 +1,9 @@
<script>
import ImageViewer from '../../content_viewer/viewers/image_viewer.vue';
-import TwoUpViewer from './image_diff/two_up_viewer.vue';
-import SwipeViewer from './image_diff/swipe_viewer.vue';
-import OnionSkinViewer from './image_diff/onion_skin_viewer.vue';
import { diffModes, imageViewMode } from '../constants';
+import OnionSkinViewer from './image_diff/onion_skin_viewer.vue';
+import SwipeViewer from './image_diff/swipe_viewer.vue';
+import TwoUpViewer from './image_diff/two_up_viewer.vue';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
index eba6dd4d14c..d6f99e9a049 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
@@ -1,8 +1,7 @@
<script>
-import { mapActions } from 'vuex';
import { GlAlert, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { mapActions } from 'vuex';
-import { __ } from '~/locale';
import {
TRANSITION_LOAD_START,
TRANSITION_LOAD_ERROR,
@@ -14,6 +13,7 @@ import {
RENAMED_DIFF_TRANSITIONS,
} from '~/diffs/constants';
import { truncateSha } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
export default {
STATE_LOADING,
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
index 2a28b13e7bf..c4dfcf93a18 100644
--- a/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
+++ b/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { slugifyWithUnderscore } from '~/lib/utils/text_utility';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
export default {
components: {
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 7e82d8f3f9c..eb8400e81c7 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -36,10 +36,8 @@ export default {
aria-expanded="false"
>
<gl-loading-icon v-show="isLoading" :inline="true" />
- <template>
- <slot v-if="$slots.default"></slot>
- <span v-else class="dropdown-toggle-text"> {{ toggleText }} </span>
- </template>
+ <slot v-if="$slots.default"></slot>
+ <span v-else class="dropdown-toggle-text"> {{ toggleText }} </span>
<gl-icon
v-show="!isLoading"
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
diff --git a/app/assets/javascripts/vue_shared/components/editor_lite.vue b/app/assets/javascripts/vue_shared/components/editor_lite.vue
index 7218b84cf8a..c3bddabea21 100644
--- a/app/assets/javascripts/vue_shared/components/editor_lite.vue
+++ b/app/assets/javascripts/vue_shared/components/editor_lite.vue
@@ -1,7 +1,7 @@
<script>
import { debounce } from 'lodash';
+import { CONTENT_UPDATE_DEBOUNCE, EDITOR_READY_EVENT } from '~/editor/constants';
import Editor from '~/editor/editor_lite';
-import { CONTENT_UPDATE_DEBOUNCE } from '~/editor/constants';
function initEditorLite({ el, ...args }) {
const editor = new Editor({
@@ -88,6 +88,7 @@ export default {
return this.editor;
},
},
+ readyEvent: EDITOR_READY_EVENT,
};
</script>
<template>
@@ -95,7 +96,7 @@ export default {
:id="`editor-lite-${fileGlobalId}`"
ref="editor"
data-editor-loading
- @editor-ready="$emit('editor-ready')"
+ @[$options.readyEvent]="$emit($options.readyEvent)"
>
<pre class="editor-loading-content">{{ value }}</pre>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index 27933f87929..4ec54b33bce 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -1,10 +1,10 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list';
-import { GlIcon } from '@gitlab/ui';
-import Item from './item.vue';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import Item from './item.vue';
export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
index 4c496ba3f9b..7816c1d74ec 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/item.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
@@ -1,8 +1,8 @@
<script>
-import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { GlIcon } from '@gitlab/ui';
-import FileIcon from '../file_icon.vue';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
import ChangedFileIcon from '../changed_file_icon.vue';
+import FileIcon from '../file_icon.vue';
const MAX_PATH_LENGTH = 60;
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 6190b07962d..8ac8a3beb7d 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import getIconForFile from './file_icon/file_icon_map';
import { FILE_SYMLINK_MODE } from '../constants';
+import getIconForFile from './file_icon/file_icon_map';
/* This is a re-usable vue component for rendering a svg sprite
icon
@@ -90,6 +90,12 @@ export default {
<svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]">
<use v-bind="{ 'xlink:href': spriteHref }" />
</svg>
- <gl-icon v-else :name="folderIconName" :size="size" class="folder-icon" />
+ <gl-icon
+ v-else
+ :name="folderIconName"
+ :size="size"
+ class="folder-icon"
+ data-qa-selector="folder_icon_content"
+ />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 96567111bbc..0b0a416b7ef 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -1,8 +1,8 @@
<script>
import { GlTruncate } from '@gitlab/ui';
-import FileHeader from '~/vue_shared/components/file_row_header.vue';
-import FileIcon from '~/vue_shared/components/file_icon.vue';
import { escapeFileUrl } from '~/lib/utils/url_utility';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import FileHeader from '~/vue_shared/components/file_row_header.vue';
export default {
name: 'FileRow',
@@ -147,6 +147,7 @@ export default {
:style="levelIndentation"
class="file-row-name"
data-qa-selector="file_name_content"
+ :data-qa-file-name="file.name"
data-testid="file-row-name-container"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index a4c5ca28494..97a8f681faf 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -10,14 +10,13 @@ import {
} from '@gitlab/ui';
import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
-import { __ } from '~/locale';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-
-import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { __ } from '~/locale';
-import { stripQuotes, uniqueTokens } from './filtered_search_utils';
import { SortDirection } from './constants';
+import { stripQuotes, uniqueTokens } from './filtered_search_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
index 411654d15f4..4dfc61f1fff 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
@@ -1,7 +1,7 @@
+import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import Api from '~/api';
import * as types from './mutation_types';
export const setEndpoints = ({ commit }, params) => {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/index.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/index.js
index 665bb29a17e..cdcbecdd313 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/index.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/index.js
@@ -1,6 +1,6 @@
-import state from './state';
import * as actions from './actions';
import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index d59e9200e6c..9c2a644b7a9 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -9,12 +9,11 @@ import {
import { debounce } from 'lodash';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { __ } from '~/locale';
-
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
-import { stripQuotes } from '../filtered_search_utils';
import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants';
+import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 0dd7820073a..cda6e4d6726 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -10,8 +10,8 @@ import { debounce } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
-import { stripQuotes } from '../filtered_search_utils';
import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants';
+import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
index 809932b0f29..44c3fc34ba6 100644
--- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
@@ -84,7 +84,7 @@ export const tributeConfig = {
value.type === groupType ? last(value.name.split(' / ')) : `${value.name}${value.username}`,
menuItemLimit: memberLimit,
menuItemTemplate: ({ original }) => {
- const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
+ const commonClasses = 'gl-avatar gl-avatar-s32 gl-flex-shrink-0';
const noAvatarClasses = `${commonClasses} gl-rounded-small
gl-display-flex gl-align-items-center gl-justify-content-center`;
@@ -111,7 +111,7 @@ export const tributeConfig = {
return `
<div class="gl-display-flex gl-align-items-center">
${avatar}
- <div class="gl-font-sm gl-line-height-normal gl-ml-3">
+ <div class="gl-line-height-normal gl-ml-4">
<div>${escape(displayName)}${count}</div>
<div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
index e14f6a04d3c..96d99faa952 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
@@ -1,6 +1,7 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlModal } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
/**
* This component keeps the GlModal's visibility in sync with the given vuex module.
@@ -46,11 +47,11 @@ export default {
},
}),
bsShow() {
- this.$root.$emit('bv::show::modal', this.modalId);
+ this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
bsHide() {
// $root.$emit is a workaround because other b-modal approaches don't work yet with gl-modal
- this.$root.$emit('bv::hide::modal', this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
cancel() {
this.$emit('cancel');
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 79d9ba6df57..80ca62a0e9b 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,11 +1,11 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlTooltipDirective, GlLink, GlButton, GlTooltip } from '@gitlab/ui';
+import { glEmojiTag } from '../../emoji';
+import { __, sprintf } from '../../locale';
import CiIconBadge from './ci_badge_link.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
-import { glEmojiTag } from '../../emoji';
-import { __, sprintf } from '../../locale';
/**
* Renders header component for job and pipeline page based on UI mockups
@@ -105,7 +105,7 @@ export default {
<section class="header-main-content">
<ci-icon-badge :status="status" />
- <strong> {{ itemName }} #{{ itemId }} </strong>
+ <strong data-testid="ci-header-item-text"> {{ itemName }} #{{ itemId }} </strong>
<template v-if="shouldRenderTriggeredLabel">{{ __('triggered') }}</template>
<template v-else>{{ __('created') }}</template>
@@ -142,13 +142,13 @@ export default {
</template>
</section>
- <section v-if="$slots.default" data-testid="headerButtons" class="gl-display-flex">
+ <section v-if="$slots.default" data-testid="ci-header-action-buttons" class="gl-display-flex">
<slot></slot>
</section>
<gl-button
v-if="hasSidebarButton"
class="d-sm-none js-sidebar-build-toggle gl-ml-auto"
- icon="angle-double-left"
+ icon="chevron-double-lg-left"
:aria-label="__('Toggle sidebar')"
@click="onClickSidebarButton"
/>
diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue
index 821ae6cec52..051c65bae70 100644
--- a/app/assets/javascripts/vue_shared/components/help_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/help_popover.vue
@@ -1,8 +1,5 @@
<script>
-import $ from 'jquery';
-import { GlButton } from '@gitlab/ui';
-import { inserted } from '~/feature_highlight/feature_highlight_helper';
-import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
+import { GlButton, GlPopover } from '@gitlab/ui';
/**
* Render a button with a question mark icon
@@ -12,6 +9,7 @@ export default {
name: 'HelpPopover',
components: {
GlButton,
+ GlPopover,
},
props: {
options: {
@@ -20,28 +18,20 @@ export default {
default: () => ({}),
},
},
- mounted() {
- const $el = $(this.$el);
-
- $el
- .popover({
- html: true,
- trigger: 'focus',
- container: 'body',
- placement: 'top',
- template:
- '<div class="popover" role="tooltip"><div class="arrow"></div><p class="popover-header"></p><div class="popover-body"></div></div>',
- ...this.options,
- })
- .on('mouseenter', mouseenter)
- .on('mouseleave', debouncedMouseleave(300))
- .on('inserted.bs.popover', inserted)
- .on('show.bs.popover', () => {
- window.addEventListener('scroll', togglePopover.bind($el, false), { once: true });
- });
- },
};
</script>
<template>
- <gl-button variant="link" icon="question" tabindex="0" />
+ <span>
+ <gl-button ref="popoverTrigger" variant="link" icon="question" tabindex="0" />
+ <gl-popover triggers="hover focus" :target="() => $refs.popoverTrigger.$el" v-bind="options">
+ <template #title>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <span v-html="options.title"></span>
+ </template>
+ <template #default>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <div v-html="options.content"></div>
+ </template>
+ </gl-popover>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue b/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
index 37995b434c4..56adbe8c606 100644
--- a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
+++ b/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
@@ -1,6 +1,6 @@
<script>
-import { mapGetters } from 'vuex';
import { GlIcon } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
index c745ea61f8b..6a0c21602bd 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -1,8 +1,8 @@
<script>
import { GlTooltip, GlIcon } from '@gitlab/ui';
+import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index 2ff4033a07e..be0c843ef00 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -2,12 +2,12 @@
/* eslint-disable vue/no-v-html */
import '~/commons/bootstrap';
import { GlIcon, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui';
-import { sprintf } from '~/locale';
-import IssueMilestone from './issue_milestone.vue';
-import IssueAssignees from './issue_assignees.vue';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
+import { sprintf } from '~/locale';
import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
import CiIcon from '../ci_icon.vue';
+import IssueAssignees from './issue_assignees.vue';
+import IssueMilestone from './issue_milestone.vue';
export default {
name: 'IssueItem',
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index b6e167524aa..25d01dc550f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,19 +1,19 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash';
-import { GlIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import { stripHtml } from '~/lib/utils/text_utility';
import { deprecatedCreateFlash as Flash } from '~/flash';
import GLForm from '~/gl_form';
-import MarkdownHeader from './header.vue';
-import MarkdownToolbar from './toolbar.vue';
+import axios from '~/lib/utils/axios_utils';
+import { stripHtml } from '~/lib/utils/text_utility';
+import { __, sprintf } from '~/locale';
import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import axios from '~/lib/utils/axios_utils';
+import MarkdownHeader from './header.vue';
+import MarkdownToolbar from './toolbar.vue';
export default {
components: {
@@ -110,11 +110,6 @@ export default {
return this.referencedUsers.length >= referencedUsersThreshold;
},
lineContent() {
- const [firstSuggestion] = this.suggestions;
- if (firstSuggestion) {
- return firstSuggestion.from_content;
- }
-
if (this.line) {
const { rich_text: richText, text } = this.line;
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 173d192dab0..5bc1786d692 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,8 +1,8 @@
<script>
-import $ from 'jquery';
import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import $ from 'jquery';
import { getSelectedFragment } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm';
import ToolbarButton from './toolbar_button.vue';
@@ -172,6 +172,7 @@ export default {
:cursor-offset="4"
:tag-content="lineContent"
icon="doc-code"
+ data-qa-selector="suggestion_button"
class="js-suggestion-btn"
@click="handleSuggestDismissed"
/>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
index 93a270b8a97..bcd8c02e968 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
@@ -1,7 +1,7 @@
<script>
+import { selectDiffLines } from '../lib/utils/diff_utils';
import SuggestionDiffHeader from './suggestion_diff_header.vue';
import SuggestionDiffRow from './suggestion_diff_row.vue';
-import { selectDiffLines } from '../lib/utils/diff_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 63341b433e0..4c6fa71398d 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -88,7 +88,12 @@ export default {
applySuggestion(message) {
if (!this.canApply) return;
this.isApplyingSingle = true;
- this.$emit('apply', this.applySuggestionCallback, message);
+
+ this.$emit(
+ 'apply',
+ this.applySuggestionCallback,
+ gon.features?.suggestionsCustomCommit ? message : undefined,
+ );
},
applySuggestionCallback() {
this.isApplyingSingle = false;
@@ -131,6 +136,7 @@ export default {
<gl-button
v-gl-tooltip.viewport="__('This also resolves all related threads')"
class="btn-inverted js-apply-batch-btn btn-grouped"
+ data-qa-selector="apply_suggestions_batch_button"
:disabled="isApplying"
variant="success"
@click="applySuggestionBatch"
@@ -145,6 +151,7 @@ export default {
<gl-button
v-if="suggestionsCount > 1 && canBeBatched && !isDisableButton"
class="btn-inverted js-add-to-batch-btn btn-grouped"
+ data-qa-selector="add_suggestion_batch_button"
:disabled="isDisableButton"
@click="addSuggestionToBatch"
>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 5ee51764555..53d1cca7af3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -1,9 +1,9 @@
<script>
-import Vue from 'vue';
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import Vue from 'vue';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
-import { deprecatedCreateFlash as Flash } from '~/flash';
export default {
directives: {
@@ -64,6 +64,11 @@ export default {
mounted() {
this.renderSuggestions();
},
+ beforeDestroy() {
+ if (this.suggestionsWatch) {
+ this.suggestionsWatch();
+ }
+ },
methods: {
renderSuggestions() {
// swaps out suggestion(s) markdown with rich diff components
@@ -108,6 +113,13 @@ export default {
},
});
+ // We're using `$watch` as `suggestionsCount` updates do not
+ // propagate to this component for some unknown reason while
+ // using a traditional prop watcher.
+ this.suggestionsWatch = this.$watch('suggestionsCount', () => {
+ suggestionDiff.suggestionsCount = this.suggestionsCount;
+ });
+
suggestionDiff.$on('apply', ({ suggestionId, callback, message }) => {
this.$emit('apply', { suggestionId, callback, flashContainer: this.$el, message });
});
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 15c5b9d6733..387b100a04f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -60,9 +60,7 @@ export default {
</div>
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">
- <template>
- <gl-icon name="media" />
- </template>
+ <gl-icon name="media" />
<span class="attaching-file-message"></span>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<span class="uploading-progress">0%</span>
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
index e3a7f144321..7b36d57dfbf 100644
--- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -2,6 +2,7 @@
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Clipboard from 'clipboard';
import { uniqueId } from 'lodash';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
export default {
components: {
@@ -76,7 +77,7 @@ export default {
});
this.clipboard
.on('success', (e) => {
- this.$root.$emit('bv::hide::tooltip', this.id);
+ this.$root.$emit(BV_HIDE_TOOLTIP, this.id);
this.$emit('success', e);
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index 653ee7f20e9..a069d1cd756 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -1,6 +1,6 @@
<script>
-import $ from 'jquery';
import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
+import $ from 'jquery';
/**
* Given an array of tabs, renders non linked bootstrap tabs.
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 cc1203f83f0..50972a8c32c 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -18,8 +18,6 @@
* }"
* />
*/
-import $ from 'jquery';
-import { mapGetters, mapActions, mapState } from 'vuex';
import {
GlButton,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
@@ -27,12 +25,14 @@ import {
GlIcon,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
+import $ from 'jquery';
+import { mapGetters, mapActions, mapState } from 'vuex';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
+import initMRPopovers from '~/mr_popover/';
import noteHeader from '~/notes/components/note_header.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
-import initMRPopovers from '~/mr_popover/';
+import TimelineEntryItem from './timeline_entry_item.vue';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index d03987bbbe0..e2591362611 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -1,13 +1,13 @@
<script>
import { GlAlert, GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
import Api from '~/api';
-import Tracking from '~/tracking';
-import { __ } from '~/locale';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
-import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
-import { isAny } from './utils';
+import { __ } from '~/locale';
+import Tracking from '~/tracking';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
+import { isAny } from './utils';
export default {
defaultI18n,
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
index 65bd4e4382d..ddc8bbf9b27 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
@@ -2,9 +2,9 @@
/* eslint-disable vue/no-v-html */
import { GlButton, GlIcon } from '@gitlab/ui';
import { isString } from 'lodash';
-import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
+import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
export default {
name: 'ProjectListItem',
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index e659e2155fb..a0c5a0559de 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -1,6 +1,6 @@
<script>
-import { debounce } from 'lodash';
import { GlLoadingIcon, GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
+import { debounce } from 'lodash';
import { __, n__, sprintf } from '~/locale';
import ProjectListItem from './project_list_item.vue';
diff --git a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
index 08ee23d25bf..bc7f8a2b17a 100644
--- a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue
@@ -1,7 +1,7 @@
<script>
import { uniqueId } from 'lodash';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Tracking from '~/tracking';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
name: 'CodeInstruction',
diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
index 8965dba3e83..9db5d6953d7 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -54,19 +54,17 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1"
:class="optionalClasses"
>
- <div class="gl-display-flex gl-align-items-center gl-py-5">
+ <div class="gl-display-flex gl-align-items-center gl-py-3">
<div
v-if="$slots['left-action']"
- class="gl-w-7 gl-display-none gl-display-sm-flex gl-justify-content-start gl-pl-2"
+ class="gl-w-7 gl-display-none gl-sm-display-flex gl-justify-content-start gl-pl-2"
>
<slot name="left-action"></slot>
</div>
<div
class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
>
- <div
- class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
- >
+ <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1">
<div
v-if="$slots['left-primary']"
class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
@@ -77,13 +75,13 @@ export default {
:selected="isDetailsShown"
icon="ellipsis_h"
size="small"
- class="gl-ml-2 gl-display-none gl-display-sm-block"
+ class="gl-ml-2 gl-display-none gl-sm-display-block"
@click="toggleDetails"
/>
</div>
<div
v-if="$slots['left-secondary']"
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
>
<slot name="left-secondary"></slot>
</div>
@@ -99,7 +97,7 @@ export default {
</div>
<div
v-if="$slots['right-secondary']"
- class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6"
+ class="gl-display-flex gl-align-items-center gl-min-h-6"
>
<slot name="right-secondary"></slot>
</div>
@@ -107,7 +105,7 @@ export default {
</div>
<div
v-if="$slots['right-action']"
- class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1"
+ class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
>
<slot name="right-action"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
new file mode 100644
index 00000000000..62453a25f62
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
@@ -0,0 +1,90 @@
+<script>
+import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
+
+const ASCENDING_ORDER = 'asc';
+const DESCENDING_ORDER = 'desc';
+
+export default {
+ components: {
+ GlSorting,
+ GlSortingItem,
+ GlFilteredSearch,
+ },
+ props: {
+ filter: {
+ type: Array,
+ required: true,
+ },
+ sorting: {
+ type: Object,
+ required: true,
+ },
+ tokens: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ sortableFields: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ internalFilter: {
+ get() {
+ return this.filter;
+ },
+ set(value) {
+ this.$emit('filter:changed', value);
+ },
+ },
+ sortText() {
+ const field = this.sortableFields.find((s) => s.orderBy === this.sorting.orderBy);
+ return field ? field.label : '';
+ },
+ isSortAscending() {
+ return this.sorting.sort === ASCENDING_ORDER;
+ },
+ },
+ methods: {
+ onDirectionChange() {
+ const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ORDER;
+ this.$emit('sorting:changed', { sort });
+ },
+ onSortItemClick(item) {
+ this.$emit('sorting:changed', { orderBy: item });
+ },
+ clearSearch() {
+ this.$emit('filter:changed', []);
+ this.$emit('filter:submit');
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100">
+ <gl-filtered-search
+ v-model="internalFilter"
+ class="gl-mr-4 gl-flex-fill-1"
+ :placeholder="__('Filter results')"
+ :available-tokens="tokens"
+ @submit="$emit('filter:submit')"
+ @clear="clearSearch"
+ />
+ <gl-sorting
+ :text="sortText"
+ :is-ascending="isSortAscending"
+ @sortDirectionChange="onDirectionChange"
+ >
+ <gl-sorting-item
+ v-for="item in sortableFields"
+ ref="packageListSortItem"
+ :key="item.orderBy"
+ @click="onSortItemClick(item.orderBy)"
+ >
+ {{ item.label }}
+ </gl-sorting-item>
+ </gl-sorting>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
index 5d4c192c78f..8988dab85d2 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
@@ -2,9 +2,9 @@
import 'codemirror/lib/codemirror.css';
import '@toast-ui/editor/dist/toastui-editor.css';
+import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants';
import AddImageModal from './modals/add_image/add_image_modal.vue';
import InsertVideoModal from './modals/insert_video_modal.vue';
-import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants';
import {
registerHTMLToMarkdownRenderer,
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
index 624b5b09b38..6ffd280e005 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
@@ -1,12 +1,12 @@
import { union, mapValues } from 'lodash';
-import renderBlockHtml from './renderers/render_html_block';
+import renderAttributeDefinition from './renderers/render_attribute_definition';
+import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
import renderHeading from './renderers/render_heading';
+import renderBlockHtml from './renderers/render_html_block';
import renderIdentifierInstanceText from './renderers/render_identifier_instance_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
-import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
-import renderSoftbreak from './renderers/render_softbreak';
-import renderAttributeDefinition from './renderers/render_attribute_definition';
import renderListItem from './renderers/render_list_item';
+import renderSoftbreak from './renderers/render_softbreak';
const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
const htmlBlockRenderers = [renderBlockHtml];
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
index be78651d38d..026a4069d9b 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
@@ -1,9 +1,9 @@
-import Vue from 'vue';
import { defaults } from 'lodash';
+import Vue from 'vue';
+import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants';
import ToolbarItem from '../toolbar_item.vue';
-import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
import buildCustomHTMLRenderer from './build_custom_renderer';
-import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants';
+import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
import sanitizeHTML from './sanitize_html';
const buildWrapper = (propsData) => {
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
index 30012c1123f..710b807275b 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
@@ -1,6 +1,6 @@
-import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
-import { ALLOWED_VIDEO_ORIGINS } from '../../constants';
import { getURLOrigin } from '~/lib/utils/url_utility';
+import { ALLOWED_VIDEO_ORIGINS } from '../../constants';
+import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
const isVideoFrame = (html) => {
const parser = new DOMParser();
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
index cb0f1d51cb1..486d88466b7 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js
@@ -1,6 +1,6 @@
import createSanitizer from 'dompurify';
-import { ALLOWED_VIDEO_ORIGINS } from '../constants';
import { getURLOrigin } from '~/lib/utils/url_utility';
+import { ALLOWED_VIDEO_ORIGINS } from '../constants';
const sanitizer = createSanitizer(window);
const ADD_TAGS = ['iframe'];
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
new file mode 100644
index 00000000000..facace0d809
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
@@ -0,0 +1,18 @@
+import { s__ } from '~/locale';
+
+export const PLATFORMS_WITHOUT_ARCHITECTURES = ['docker', 'kubernetes'];
+
+export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = {
+ docker: {
+ instructions: s__(
+ 'Runners|To install Runner in a container follow the instructions described in the GitLab documentation',
+ ),
+ link: 'https://docs.gitlab.com/runner/install/docker.html',
+ },
+ kubernetes: {
+ instructions: s__(
+ 'Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation.',
+ ),
+ link: 'https://docs.gitlab.com/runner/install/kubernetes.html',
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql
new file mode 100644
index 00000000000..ff0626167a9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql
@@ -0,0 +1,20 @@
+query getRunnerPlatforms($projectPath: ID!, $groupPath: ID!) {
+ runnerPlatforms {
+ nodes {
+ name
+ humanReadableName
+ architectures {
+ nodes {
+ name
+ downloadLocation
+ }
+ }
+ }
+ }
+ project(fullPath: $projectPath) {
+ id
+ }
+ group(fullPath: $groupPath) {
+ id
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql
new file mode 100644
index 00000000000..643c1991807
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql
@@ -0,0 +1,16 @@
+query runnerSetupInstructions(
+ $platform: String!
+ $architecture: String!
+ $projectId: ID!
+ $groupId: ID!
+) {
+ runnerSetup(
+ platform: $platform
+ architecture: $architecture
+ projectId: $projectId
+ groupId: $groupId
+ ) {
+ installInstructions
+ registerInstructions
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
new file mode 100644
index 00000000000..1d6db576942
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
@@ -0,0 +1,261 @@
+<script>
+import {
+ GlAlert,
+ GlButton,
+ GlModal,
+ GlModalDirective,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+} from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import {
+ PLATFORMS_WITHOUT_ARCHITECTURES,
+ INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
+} from './constants';
+import getRunnerPlatforms from './graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructions from './graphql/queries/get_runner_setup.query.graphql';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlIcon,
+ ModalCopyButton,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ groupPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ runnerPlatforms: {
+ query: getRunnerPlatforms,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ groupPath: this.groupPath,
+ };
+ },
+ error() {
+ this.showAlert = true;
+ },
+ result({ data }) {
+ this.project = data?.project;
+ this.group = data?.group;
+
+ this.selectPlatform(this.platforms[0].name);
+ },
+ },
+ },
+ data() {
+ return {
+ showAlert: false,
+ selectedPlatformArchitectures: [],
+ selectedPlatform: {
+ name: '',
+ },
+ selectedArchitecture: {},
+ runnerPlatforms: {},
+ instructions: {},
+ project: {},
+ group: {},
+ };
+ },
+ computed: {
+ isPlatformSelected() {
+ return Object.keys(this.selectedPlatform).length > 0;
+ },
+ instructionsEmpty() {
+ return Object.keys(this.instructions).length === 0;
+ },
+ groupId() {
+ return this.group?.id ?? '';
+ },
+ projectId() {
+ return this.project?.id ?? '';
+ },
+ platforms() {
+ return this.runnerPlatforms?.nodes;
+ },
+ hasArchitecureList() {
+ return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatform?.name);
+ },
+ instructionsWithoutArchitecture() {
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.instructions;
+ },
+ runnerInstallationLink() {
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.link;
+ },
+ },
+ methods: {
+ selectPlatform(name) {
+ this.selectedPlatform = this.platforms.find((platform) => platform.name === name);
+ if (this.hasArchitecureList) {
+ this.selectedPlatformArchitectures = this.selectedPlatform?.architectures?.nodes;
+ [this.selectedArchitecture] = this.selectedPlatformArchitectures;
+ this.selectArchitecture(this.selectedArchitecture);
+ }
+ },
+ selectArchitecture(architecture) {
+ this.selectedArchitecture = architecture;
+
+ this.$apollo.addSmartQuery('instructions', {
+ variables() {
+ return {
+ platform: this.selectedPlatform.name,
+ architecture: this.selectedArchitecture.name,
+ projectId: this.projectId,
+ groupId: this.groupId,
+ };
+ },
+ query: getRunnerSetupInstructions,
+ update(data) {
+ return data?.runnerSetup;
+ },
+ error() {
+ this.showAlert = true;
+ },
+ });
+ },
+ toggleAlert(state) {
+ this.showAlert = state;
+ },
+ },
+ modalId: 'installation-instructions-modal',
+ i18n: {
+ installARunner: s__('Runners|Install a Runner'),
+ architecture: s__('Runners|Architecture'),
+ downloadInstallBinary: s__('Runners|Download and Install Binary'),
+ downloadLatestBinary: s__('Runners|Download Latest Binary'),
+ registerRunner: s__('Runners|Register Runner'),
+ method: __('Method'),
+ fetchError: s__('Runners|An error has occurred fetching instructions'),
+ instructions: s__('Runners|Show Runner installation instructions'),
+ copyInstructions: s__('Runners|Copy instructions'),
+ },
+ closeButton: {
+ text: __('Close'),
+ attributes: [{ variant: 'default' }],
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-button
+ v-gl-modal-directive="$options.modalId"
+ class="gl-mt-4"
+ data-testid="show-modal-button"
+ >
+ {{ $options.i18n.instructions }}
+ </gl-button>
+ <gl-modal
+ :modal-id="$options.modalId"
+ :title="$options.i18n.installARunner"
+ :action-secondary="$options.closeButton"
+ >
+ <gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
+ {{ $options.i18n.fetchError }}
+ </gl-alert>
+ <h5>{{ __('Environment') }}</h5>
+ <gl-button-group class="gl-mb-5">
+ <gl-button
+ v-for="platform in platforms"
+ :key="platform.name"
+ data-testid="platform-button"
+ @click="selectPlatform(platform.name)"
+ >
+ {{ platform.humanReadableName }}
+ </gl-button>
+ </gl-button-group>
+ <template v-if="hasArchitecureList">
+ <template v-if="isPlatformSelected">
+ <h5>
+ {{ $options.i18n.architecture }}
+ </h5>
+ <gl-dropdown class="gl-mb-5" :text="selectedArchitecture.name">
+ <gl-dropdown-item
+ v-for="architecture in selectedPlatformArchitectures"
+ :key="architecture.name"
+ data-testid="architecture-dropdown-item"
+ @click="selectArchitecture(architecture)"
+ >
+ {{ architecture.name }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <div class="gl-display-flex gl-align-items-center gl-mb-5">
+ <h5>{{ $options.i18n.downloadInstallBinary }}</h5>
+ <gl-button
+ class="gl-ml-auto"
+ :href="selectedArchitecture.downloadLocation"
+ download
+ data-testid="binary-download-button"
+ >
+ {{ $options.i18n.downloadLatestBinary }}
+ </gl-button>
+ </div>
+ </template>
+ <template v-if="!instructionsEmpty">
+ <div class="gl-display-flex">
+ <pre
+ class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
+ data-testid="binary-instructions"
+ >
+
+ {{ instructions.installInstructions }}
+ </pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="instructions.installInstructions"
+ :modal-id="$options.modalId"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+
+ <hr />
+ <h5 class="gl-mb-5">{{ $options.i18n.registerRunner }}</h5>
+ <h5 class="gl-mb-5">{{ $options.i18n.method }}</h5>
+ <div class="gl-display-flex">
+ <pre
+ class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
+ data-testid="runner-instructions"
+ >
+ {{ instructions.registerInstructions }}
+ </pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="instructions.registerInstructions"
+ :modal-id="$options.modalId"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+ </template>
+ </template>
+ <template v-else>
+ <div>
+ <p>{{ instructionsWithoutArchitecture }}</p>
+ <gl-button :href="runnerInstallationLink">
+ <gl-icon name="external-link" />
+ {{ s__('Runners|View installation instructions') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/settings/settings_block.vue b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
new file mode 100644
index 00000000000..31094b985a2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: { GlButton },
+ props: {
+ defaultExpanded: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ sectionExpanded: false,
+ };
+ },
+ computed: {
+ expanded() {
+ return this.defaultExpanded || this.sectionExpanded;
+ },
+ toggleText() {
+ return this.expanded ? __('Collapse') : __('Expand');
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="settings no-animate" :class="{ expanded }">
+ <div class="settings-header">
+ <h4><slot name="title"></slot></h4>
+ <gl-button @click="sectionExpanded = !sectionExpanded">
+ {{ toggleText }}
+ </gl-button>
+ <p>
+ <slot name="description"></slot>
+ </p>
+ </div>
+ <div class="settings-content">
+ <slot></slot>
+ </div>
+ </section>
+</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 80c61627b8f..a1dca65a423 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,7 +1,7 @@
<script>
+import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
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 {
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 6caf8bc92c2..075681de320 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -1,10 +1,10 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { dateInWords } from '../../../lib/utils/datetime_utility';
import datePicker from '../pikaday.vue';
-import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
-import { dateInWords } from '../../../lib/utils/datetime_utility';
-import { __ } from '~/locale';
+import toggleSidebar from './toggle_sidebar.vue';
export default {
name: 'SidebarDatePicker',
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 22d86ee25d1..88c4d132d61 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -1,20 +1,19 @@
<script>
-import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
+import $ from 'jquery';
import LabelsSelect from '~/labels_select';
+import { __ } from '~/locale';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-import DropdownTitle from './dropdown_title.vue';
-import DropdownValue from './dropdown_value.vue';
-import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
+import { DropdownVariant } from '../labels_select_vue/constants';
import DropdownButton from './dropdown_button.vue';
+import DropdownCreateLabel from './dropdown_create_label.vue';
+import DropdownFooter from './dropdown_footer.vue';
import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
-import DropdownFooter from './dropdown_footer.vue';
-import DropdownCreateLabel from './dropdown_create_label.vue';
-
-import { DropdownVariant } from '../labels_select_vue/constants';
+import DropdownTitle from './dropdown_title.vue';
+import DropdownValue from './dropdown_value.vue';
+import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
export default {
DropdownVariant,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
index c65266fce5a..60111210f5d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
import { GlButton, GlIcon } from '@gitlab/ui';
+import { mapActions, mapGetters } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
index 267c3be5f50..e3704198ad0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
@@ -1,8 +1,8 @@
<script>
import { mapState } from 'vuex';
-import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
+import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
index 41308e352e3..f8cc981ba3d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index a365673f7a1..6065b6c160c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -1,5 +1,4 @@
<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
import {
GlIntersectionObserver,
GlLoadingIcon,
@@ -8,6 +7,7 @@ import {
GlLink,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { mapState, mapGetters, mapActions } from 'vuex';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
index 2d6a4a9758c..5d1663bc1fd 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState, mapActions } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
index a6f99289df4..f173c8db540 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
@@ -1,6 +1,6 @@
<script>
-import { mapState } from 'vuex';
import { GlLabel } from '@gitlab/ui';
+import { mapState } from 'vuex';
import { isScopedLabel } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index 683889b8611..93fdae19a8d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -7,14 +7,12 @@ import { __ } from '~/locale';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
-import labelsSelectModule from './store';
-
-import DropdownTitle from './dropdown_title.vue';
-import DropdownValue from './dropdown_value.vue';
+import { DropdownVariant } from './constants';
import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue';
-
-import { DropdownVariant } from './constants';
+import DropdownTitle from './dropdown_title.vue';
+import DropdownValue from './dropdown_value.vue';
+import labelsSelectModule from './store';
Vue.use(Vuex);
@@ -35,11 +33,13 @@ export default {
},
allowLabelEdit: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
allowLabelCreate: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
allowMultiselect: {
type: Boolean,
@@ -48,7 +48,8 @@ export default {
},
allowScopedLabels: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
variant: {
type: String,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
index 14b46c1c431..89f96ab916b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
@@ -1,6 +1,6 @@
import { deprecatedCreateFlash as flash } from '~/flash';
-import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
import * as types from './mutation_types';
export const setInitialState = ({ commit }, props) => commit(types.SET_INITIAL_STATE, props);
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 6de436ffd13..55716e1105e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { DropdownVariant } from '../constants';
+import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, props) {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
index c5bbe1b33fb..132abcab82b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue
@@ -20,7 +20,7 @@ export default {
</script>
<template>
- <gl-dropdown class="show" :text="text" :header-text="headerText">
+ <gl-dropdown class="show" :text="text" :header-text="headerText" @toggle="$emit('toggle')">
<slot name="search"></slot>
<gl-dropdown-form>
<slot name="items"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql
deleted file mode 100644
index 612a0c02e82..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql
+++ /dev/null
@@ -1,13 +0,0 @@
-query issueParticipants($id: IssueID!) {
- issue(id: $id) {
- participants {
- nodes {
- username
- name
- webUrl
- avatarUrl
- id
- }
- }
- }
-}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
new file mode 100644
index 00000000000..62c0b05426b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
@@ -0,0 +1,19 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+query issueParticipants($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ issuable: issue(iid: $iid) {
+ id
+ participants {
+ nodes {
+ ...User
+ }
+ }
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
new file mode 100644
index 00000000000..a75ce85a1dc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
@@ -0,0 +1,19 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+query getMrParticipants($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ issuable: mergeRequest(iid: $iid) {
+ id
+ participants {
+ nodes {
+ ...User
+ }
+ }
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
index 9ead95a3801..2eb9bb4b07b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
@@ -1,15 +1,19 @@
-mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $projectPath: ID!) {
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) {
issueSetAssignees(
- input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $projectPath }
+ input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $fullPath }
) {
issue {
+ id
assignees {
nodes {
- username
- id
- name
- webUrl
- avatarUrl
+ ...User
+ }
+ }
+ participants {
+ nodes {
+ ...User
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
new file mode 100644
index 00000000000..a0f15a07692
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
@@ -0,0 +1,21 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+mutation mergeRequestSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) {
+ mergeRequestSetAssignees(
+ input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $fullPath }
+ ) {
+ mergeRequest {
+ id
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ participants {
+ nodes {
+ ...User
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue
index 61b317d0d1d..994fa68fb1a 100644
--- a/app/assets/javascripts/vue_shared/components/split_button.vue
+++ b/app/assets/javascripts/vue_shared/components/split_button.vue
@@ -1,6 +1,6 @@
<script>
-import { isString } from 'lodash';
import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui';
+import { isString } from 'lodash';
const isValidItem = (item) =>
isString(item.eventName) && isString(item.title) && isString(item.description);
diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
index 9b6d0a87374..e639a2d966b 100644
--- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
import { roundDownFloat } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
export default {
directives: {
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
index f1db26ff4fc..b9ee74d6a03 100644
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
@@ -1,8 +1,8 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { secondsToHours } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
-import { secondsToHours } from '~/lib/utils/datetime_utility';
export default {
name: 'TimezoneDropdown',
diff --git a/app/assets/javascripts/vue_shared/components/todo_button.vue b/app/assets/javascripts/vue_shared/components/todo_button.vue
index a9d4f8403fa..935d222a1a9 100644
--- a/app/assets/javascripts/vue_shared/components/todo_button.vue
+++ b/app/assets/javascripts/vue_shared/components/todo_button.vue
@@ -15,7 +15,7 @@ export default {
},
computed: {
buttonLabel() {
- return this.isTodo ? __('Mark as done') : __('Add a To Do');
+ return this.isTodo ? __('Mark as done') : __('Add a to do');
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
deleted file mode 100644
index 861661d3519..00000000000
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<script>
-import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { s__ } from '../../locale';
-
-const ICON_ON = 'status_success_borderless';
-const ICON_OFF = 'status_failed_borderless';
-const LABEL_ON = s__('ToggleButton|Toggle Status: ON');
-const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
-
-export default {
- components: {
- GlIcon,
- GlLoadingIcon,
- },
-
- model: {
- prop: 'value',
- event: 'change',
- },
-
- props: {
- name: {
- type: String,
- required: false,
- default: null,
- },
- value: {
- type: Boolean,
- required: false,
- default: null,
- },
- disabledInput: {
- type: Boolean,
- required: false,
- default: false,
- },
- isLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- computed: {
- toggleIcon() {
- return this.value ? ICON_ON : ICON_OFF;
- },
- ariaLabel() {
- return this.value ? LABEL_ON : LABEL_OFF;
- },
- },
-
- methods: {
- toggleFeature() {
- if (!this.disabledInput) this.$emit('change', !this.value);
- },
- },
-};
-</script>
-
-<template>
- <label class="gl-mt-2">
- <input v-if="name" :name="name" :value="value" type="hidden" />
- <button
- type="button"
- role="switch"
- class="project-feature-toggle"
- :aria-label="ariaLabel"
- :aria-checked="value"
- :class="{
- 'is-checked': value,
- 'gl-blue-500': value,
- 'is-disabled': disabledInput,
- 'is-loading': isLoading,
- }"
- @click.prevent="toggleFeature"
- >
- <gl-loading-icon class="loading-icon" />
- <span class="toggle-icon">
- <gl-icon
- :size="18"
- :name="toggleIcon"
- :class="value ? 'gl-text-blue-500' : 'gl-text-gray-400'"
- />
- </span>
- </button>
- </label>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
index b48dfa8b452..8aa6e29adf1 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
@@ -1,7 +1,7 @@
<script>
import { isFunction } from 'lodash';
-import tooltip from '../directives/tooltip';
import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
+import tooltip from '../directives/tooltip';
export default {
directives: {
diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
index 01ba2cf5c39..5a08e992084 100644
--- a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
+++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue
@@ -1,8 +1,8 @@
<script>
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
-import { isValidImage } from './utils';
import { VALID_DATA_TRANSFER_TYPE, VALID_IMAGE_FILE_MIMETYPE } from './constants';
+import { isValidImage } from './utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 2ab4c55d9b0..37bde089de8 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -6,9 +6,9 @@ import {
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlIcon,
} from '@gitlab/ui';
-import UserAvailabilityStatus from '~/set_status_modal/components/user_availability_status.vue';
-import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
+import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import { glEmojiTag } from '../../../emoji';
+import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
const MAX_SKELETON_LINES = 4;
@@ -26,7 +26,7 @@ export default {
GlPopover,
GlSkeletonLoading,
UserAvatarImage,
- UserAvailabilityStatus,
+ UserNameWithStatus,
},
props: {
target: {
@@ -66,7 +66,7 @@ export default {
);
},
availabilityStatus() {
- return this.user?.status?.availability || null;
+ return this.user?.status?.availability || '';
},
},
};
@@ -93,11 +93,7 @@ export default {
<template v-else>
<div class="gl-mb-3">
<h5 class="gl-m-0">
- {{ user.name }}
- <user-availability-status
- v-if="availabilityStatus"
- :availability="availabilityStatus"
- />
+ <user-name-with-status :name="user.name" :availability="availabilityStatus" />
</h5>
<span class="gl-text-gray-500">@{{ user.username }}</span>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index c957876f8ab..4bd3e352fd2 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -1,8 +1,8 @@
<script>
import $ from 'jquery';
import { __ } from '~/locale';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
const KEY_EDIT = 'edit';
const KEY_WEB_IDE = 'webide';
diff --git a/app/assets/javascripts/vue_shared/directives/validation.js b/app/assets/javascripts/vue_shared/directives/validation.js
index ece09df272c..176954891e9 100644
--- a/app/assets/javascripts/vue_shared/directives/validation.js
+++ b/app/assets/javascripts/vue_shared/directives/validation.js
@@ -86,7 +86,7 @@ const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = fa
* @param {Object<string, { message: string, isValid: ?function}>} customFeedbackMap
* @returns {{ inserted: function, update: function }} validateDirective
*/
-export default function (customFeedbackMap = {}) {
+export default function initValidation(customFeedbackMap = {}) {
const feedbackMap = merge(defaultFeedbackMap, customFeedbackMap);
const elDataMap = new WeakMap();
diff --git a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
index 56da2637825..52ded0e0cc1 100644
--- a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
@@ -1,6 +1,6 @@
import { isEmpty } from 'lodash';
-import { sprintf, __ } from '~/locale';
import { formatDate } from '~/lib/utils/datetime_utility';
+import { sprintf, __ } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
const mixins = {
diff --git a/app/assets/javascripts/vue_shared/plugins/global_toast.js b/app/assets/javascripts/vue_shared/plugins/global_toast.js
index 7a2e5d80a5d..bfea2bedd40 100644
--- a/app/assets/javascripts/vue_shared/plugins/global_toast.js
+++ b/app/assets/javascripts/vue_shared/plugins/global_toast.js
@@ -1,5 +1,5 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
+import Vue from 'vue';
Vue.use(GlToast);
const instance = new Vue();
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index dd591f7bba3..aac5a5c1def 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -17,7 +17,13 @@ export const REPORT_FILE_TYPES = {
* Security scan report types, as provided by the backend.
*/
export const REPORT_TYPE_SAST = 'sast';
+export const REPORT_TYPE_DAST = 'dast';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
+export const REPORT_TYPE_DEPENDENCY_SCANNING = 'dependency_scanning';
+export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning';
+export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing';
+export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_compliance';
+export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
/**
* SecurityReportTypeEnum values for use with GraphQL.
diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
index a6c7b59aa71..b27dd33835f 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -1,31 +1,26 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import ReportSection from '~/reports/components/report_section.vue';
-import { LOADING, ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
-import { s__ } from '~/locale';
-import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
-import Api from '~/api';
+import { s__ } from '~/locale';
+import ReportSection from '~/reports/components/report_section.vue';
+import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import HelpIcon from './components/help_icon.vue';
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
import SecuritySummary from './components/security_summary.vue';
-import store from './store';
-import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_SECRET_DETECTION,
reportTypeToSecurityReportTypeEnum,
} from './constants';
import securityReportDownloadPathsQuery from './queries/security_report_download_paths.query.graphql';
+import store from './store';
+import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
import { extractSecurityReportArtifacts } from './utils';
export default {
store,
components: {
- GlLink,
- GlSprintf,
ReportSection,
HelpIcon,
SecurityReportDownloadDropdown,
@@ -101,9 +96,6 @@ export default {
),
};
},
- skip() {
- return !this.canShowDownloads;
- },
update(data) {
return extractSecurityReportArtifacts(this.$options.reportTypes, data);
},
@@ -124,9 +116,6 @@ export default {
},
computed: {
...mapGetters(['groupedSummaryText', 'summaryStatus']),
- canShowDownloads() {
- return this.glFeatures.coreSecurityMrWidgetDownloads;
- },
hasSecurityReports() {
return this.availableSecurityReports.length > 0;
},
@@ -139,23 +128,6 @@ export default {
isLoadingReportArtifacts() {
return this.$apollo.queries.reportArtifacts.loading;
},
- shouldShowDownloadGuidance() {
- return !this.canShowDownloads && this.summaryStatus !== LOADING;
- },
- scansHaveRunMessage() {
- return this.canShowDownloads
- ? this.$options.i18n.scansHaveRun
- : this.$options.i18n.scansHaveRunWithDownloadGuidance;
- },
- },
- created() {
- if (!this.canShowDownloads) {
- this.checkAvailableSecurityReports(this.$options.reportTypes)
- .then((availableSecurityReports) => {
- this.onCheckingAvailableSecurityReports(Array.from(availableSecurityReports));
- })
- .catch(this.showError);
- }
},
methods: {
...mapActions(MODULE_SAST, {
@@ -166,36 +138,6 @@ export default {
setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
fetchSecretDetectionDiff: 'fetchDiff',
}),
- async checkAvailableSecurityReports(reportTypes) {
- const reportTypesSet = new Set(reportTypes);
- const availableReportTypes = new Set();
-
- let page = 1;
- while (page) {
- // eslint-disable-next-line no-await-in-loop
- const { data: jobs, headers } = await Api.pipelineJobs(this.projectId, this.pipelineId, {
- per_page: 100,
- page,
- });
-
- jobs.forEach(({ artifacts = [] }) => {
- artifacts.forEach(({ file_type }) => {
- if (reportTypesSet.has(file_type)) {
- availableReportTypes.add(file_type);
- }
- });
- });
-
- // If we've found artifacts for all the report types, stop looking!
- if (availableReportTypes.size === reportTypesSet.size) {
- return availableReportTypes;
- }
-
- page = parseIntPagination(normalizeHeaders(headers)).nextPage;
- }
-
- return availableReportTypes;
- },
fetchCounts() {
if (!this.glFeatures.coreSecurityMrWidgetCounts) {
return;
@@ -213,11 +155,6 @@ export default {
this.canShowCounts = true;
}
},
- activatePipelinesTab() {
- if (window.mrTabs) {
- window.mrTabs.tabShown('pipelines');
- }
- },
onCheckingAvailableSecurityReports(availableSecurityReports) {
this.availableSecurityReports = availableSecurityReports;
this.fetchCounts();
@@ -236,12 +173,6 @@ export default {
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
),
scansHaveRun: s__('SecurityReports|Security scans have run'),
- scansHaveRunWithDownloadGuidance: s__(
- 'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
- ),
- downloadFromPipelineTab: s__(
- 'SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
- ),
},
summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
};
@@ -265,22 +196,7 @@ export default {
</span>
</template>
- <template v-if="shouldShowDownloadGuidance" #sub-heading>
- <span class="gl-font-sm">
- <gl-sprintf :message="$options.i18n.downloadFromPipelineTab">
- <template #link="{ content }">
- <gl-link
- class="gl-font-sm"
- data-testid="show-pipelines"
- @click="activatePipelinesTab"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- </span>
- </template>
-
- <template v-if="canShowDownloads" #action-buttons>
+ <template #action-buttons>
<security-report-download-dropdown
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
@@ -298,13 +214,7 @@ export default {
data-testid="security-mr-widget"
>
<template #error>
- <gl-sprintf :message="scansHaveRunMessage">
- <template #link="{ content }">
- <gl-link data-testid="show-pipelines" @click="activatePipelinesTab">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
+ {{ $options.i18n.scansHaveRun }}
<help-icon
:help-path="securityReportsDocsPath"
@@ -312,7 +222,7 @@ export default {
/>
</template>
- <template v-if="canShowDownloads" #action-buttons>
+ <template #action-buttons>
<security-report-download-dropdown
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/getters.js b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
index 443255b0e6a..08f6bcca15b 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/getters.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
@@ -1,7 +1,7 @@
import { s__, sprintf } from '~/locale';
-import { countVulnerabilities, groupedTextBuilder } from './utils';
import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
import { TRANSLATION_IS_LOADING } from './messages';
+import { countVulnerabilities, groupedTextBuilder } from './utils';
export const summaryCounts = (state) =>
countVulnerabilities(
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/index.js b/app/assets/javascripts/vue_shared/security_reports/store/index.js
index 10705e04a21..164faa86744 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/index.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/index.js
@@ -1,9 +1,9 @@
import Vuex from 'vuex';
-import * as getters from './getters';
-import state from './state';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
+import * as getters from './getters';
import sast from './modules/sast';
import secretDetection from './modules/secret_detection';
+import state from './state';
export default () =>
new Vuex.Store({
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
index 0f26e3c30ef..4f92e181f9f 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { fetchDiffData } from '../../utils';
+import * as types from './mutation_types';
export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path);
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js
index 68c81bb4509..1d5af1d4fe5 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/index.js
@@ -1,6 +1,6 @@
-import state from './state';
-import mutations from './mutations';
import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js
index 5f6153ca3b1..11aa71d2b6b 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/mutations.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import * as types from './mutation_types';
import { parseDiff } from '../../utils';
+import * as types from './mutation_types';
export default {
[types.SET_DIFF_ENDPOINT](state, path) {
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js
index 68c81bb4509..1d5af1d4fe5 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/index.js
@@ -1,6 +1,6 @@
-import state from './state';
-import mutations from './mutations';
import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
export default {
namespaced: true,
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
index fd6613ae11c..458bacce915 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
@@ -1,5 +1,5 @@
-import pollUntilComplete from '~/lib/utils/poll_until_complete';
import axios from '~/lib/utils/axios_utils';
+import pollUntilComplete from '~/lib/utils/poll_until_complete';
import { __, n__, sprintf } from '~/locale';
import { CRITICAL, HIGH } from '~/vulnerabilities/constants';
import {
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/index.js b/app/assets/javascripts/vuex_shared/modules/modal/index.js
index c349d875c24..21ebfb22e2e 100644
--- a/app/assets/javascripts/vuex_shared/modules/modal/index.js
+++ b/app/assets/javascripts/vuex_shared/modules/modal/index.js
@@ -1,6 +1,6 @@
-import state from './state';
-import mutations from './mutations';
import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
export default () => ({
namespaced: true,
diff --git a/app/assets/javascripts/webpack_non_compiled_placeholder.js b/app/assets/javascripts/webpack_non_compiled_placeholder.js
new file mode 100644
index 00000000000..8cd1d2eb2ca
--- /dev/null
+++ b/app/assets/javascripts/webpack_non_compiled_placeholder.js
@@ -0,0 +1,24 @@
+const div = document.createElement('div');
+
+Object.assign(div.style, {
+ width: '100vw',
+ height: '100vh',
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ 'z-index': 100000,
+ background: 'rgba(0,0,0,0.9)',
+ 'font-size': '25px',
+ 'font-family': 'monospace',
+ color: 'white',
+ padding: '2.5em',
+ 'text-align': 'center',
+});
+
+div.innerHTML = `
+<h1 style="color:white">🧙 Webpack is doing its magic 🧙</h1>
+<p>If you use Hot Module reloading, the page will reload in a few seconds.</p>
+<p>If you do not use Hot Module reloading, please <a href="">reload the page manually in a few seconds</a></p>
+`;
+
+document.body.append(div);
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
index 0a81f172fe9..f4ac4f81eac 100644
--- a/app/assets/javascripts/whats_new/components/app.vue
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -1,5 +1,4 @@
<script>
-import { mapState, mapActions } from 'vuex';
import {
GlDrawer,
GlInfiniteScroll,
@@ -9,10 +8,11 @@ import {
GlBadge,
GlLoadingIcon,
} from '@gitlab/ui';
-import SkeletonLoader from './skeleton_loader.vue';
-import Feature from './feature.vue';
+import { mapState, mapActions } from 'vuex';
import Tracking from '~/tracking';
import { getDrawerBodyHeight } from '../utils/get_drawer_body_height';
+import Feature from './feature.vue';
+import SkeletonLoader from './skeleton_loader.vue';
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/whats_new/store/actions.js b/app/assets/javascripts/whats_new/store/actions.js
index 0e5eeda742a..4b3cfa55977 100644
--- a/app/assets/javascripts/whats_new/store/actions.js
+++ b/app/assets/javascripts/whats_new/store/actions.js
@@ -1,6 +1,6 @@
-import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import * as types from './mutation_types';
export default {
closeDrawer({ commit }) {
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index 06ba2496a99..134c2858849 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -5,8 +5,8 @@
/*= provides zen_mode:enter */
/*= provides zen_mode:leave */
-import $ from 'jquery';
import Dropzone from 'dropzone';
+import $ from 'jquery';
import Mousetrap from 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause';
import { scrollToElement } from '~/lib/utils/common_utils';
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 42d15635566..20ff78d32d3 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -1,4 +1,3 @@
-@import './pages/admin';
@import './pages/branches';
@import './pages/ci_projects';
@import './pages/clusters';
diff --git a/app/assets/stylesheets/components/batch_comments/review_bar.scss b/app/assets/stylesheets/components/batch_comments/review_bar.scss
index 76bf7ac81e8..d769ea73101 100644
--- a/app/assets/stylesheets/components/batch_comments/review_bar.scss
+++ b/app/assets/stylesheets/components/batch_comments/review_bar.scss
@@ -4,7 +4,7 @@
left: 0;
width: 100%;
background: $white;
- z-index: 300;
+ z-index: $zindex-dropdown-menu;
padding: 7px 0 6px; // to keep aligned with "collapse sidebar" button on the left sidebar
border-top: 1px solid $border-color;
padding-left: $contextual-sidebar-width;
diff --git a/app/assets/stylesheets/components/design_management/design_list_item.scss b/app/assets/stylesheets/components/design_management/design_list_item.scss
index b7f6b2026fe..09af4da37e9 100644
--- a/app/assets/stylesheets/components/design_management/design_list_item.scss
+++ b/app/assets/stylesheets/components/design_management/design_list_item.scss
@@ -8,11 +8,6 @@
top: 10px;
}
- .design-event {
- top: $gl-padding;
- right: $gl-padding;
- }
-
.card-body {
height: 230px;
}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index e40b95cdce6..7931f4deea0 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -15,7 +15,6 @@
@import 'framework/badges';
@import 'framework/calendar';
@import 'framework/callout';
-@import 'framework/carousel';
@import 'framework/common';
@import 'framework/dropdowns';
@import 'framework/files';
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index d9ad4992458..a7623b65539 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -14,7 +14,7 @@
top: 0;
margin-top: 3px;
padding: $gl-padding;
- z-index: 300;
+ z-index: $zindex-dropdown-menu;
width: $award-emoji-width;
font-size: 14px;
background-color: $white;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 182c58c3931..b51fec925cb 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -95,8 +95,6 @@
&:active,
&.active {
- box-shadow: $gl-btn-active-background;
-
background-color: $dark;
border-color: $border-dark;
color: $color;
@@ -348,14 +346,6 @@
}
}
-.btn-build {
- margin-left: 10px;
-
- svg {
- fill: $gl-text-color-secondary;
- }
-}
-
.clone-dropdown-btn a {
color: $gray-700;
@@ -437,19 +427,6 @@
}
}
-.btn-missing {
- color: $gl-text-color-secondary;
- border: 1px dashed $border-gray-normal-dashed;
- border-radius: $border-radius-default;
-
- &:hover,
- &:active,
- &:focus {
- color: $gl-text-color-secondary;
- background-color: $white-normal;
- }
-}
-
// The .btn-svg class is available for legacy icon buttons to
// preserve a 34px height and have 16x16 icons at the same time.
// Once a button is migrated (to the current 32px height)
diff --git a/app/assets/stylesheets/framework/carousel.scss b/app/assets/stylesheets/framework/carousel.scss
deleted file mode 100644
index d51a9f9c173..00000000000
--- a/app/assets/stylesheets/framework/carousel.scss
+++ /dev/null
@@ -1,202 +0,0 @@
-// Notes on the classes:
-//
-// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically)
-// even when their scroll action started on a carousel, but for compatibility (with Firefox)
-// we're preventing all actions instead
-// 2. The .carousel-item-left and .carousel-item-right is used to indicate where
-// the active slide is heading.
-// 3. .active.carousel-item is the current slide.
-// 4. .active.carousel-item-left and .active.carousel-item-right is the current
-// slide in its in-transition state. Only one of these occurs at a time.
-// 5. .carousel-item-next.carousel-item-left and .carousel-item-prev.carousel-item-right
-// is the upcoming slide in transition.
-
-.carousel {
- position: relative;
-
- &.pointer-event {
- touch-action: pan-y;
- }
-}
-
-
-.carousel-inner {
- position: relative;
- width: 100%;
- overflow: hidden;
- @include clearfix();
-}
-
-.carousel-item {
- position: relative;
- display: none;
- float: left;
- width: 100%;
- margin-right: -100%;
- backface-visibility: hidden;
- @include transition($carousel-transition);
-}
-
-.carousel-item.active,
-.carousel-item-next,
-.carousel-item-prev {
- display: block;
-}
-
-.carousel-item-next:not(.carousel-item-left),
-.active.carousel-item-right {
- transform: translateX(100%);
-}
-
-.carousel-item-prev:not(.carousel-item-right),
-.active.carousel-item-left {
- transform: translateX(-100%);
-}
-
-
-//
-// Alternate transitions
-//
-
-.carousel-fade {
- .carousel-item {
- opacity: 0;
- transition-property: opacity;
- transform: none;
- }
-
- .carousel-item.active,
- .carousel-item-next.carousel-item-left,
- .carousel-item-prev.carousel-item-right {
- z-index: 1;
- opacity: 1;
- }
-
- .active.carousel-item-left,
- .active.carousel-item-right {
- z-index: 0;
- opacity: 0;
- @include transition(0s $carousel-transition-duration opacity);
- }
-}
-
-
-//
-// Left/right controls for nav
-//
-
-.carousel-control-prev,
-.carousel-control-next {
- position: absolute;
- top: 0;
- bottom: 0;
- z-index: 1;
- // Use flex for alignment (1-3)
- display: flex; // 1. allow flex styles
- align-items: center; // 2. vertically center contents
- justify-content: center; // 3. horizontally center contents
- width: $carousel-control-width;
- color: $carousel-control-color;
- text-align: center;
- opacity: $carousel-control-opacity;
- @include transition($carousel-control-transition);
-
- // Hover/focus state
- @include hover-focus {
- color: $carousel-control-color;
- text-decoration: none;
- outline: 0;
- opacity: $carousel-control-hover-opacity;
- }
-}
-
-.carousel-control-prev {
- left: 0;
- @if $enable-gradients {
- background: linear-gradient(90deg, rgba($black, 0.25), rgba($black, 0.001));
- }
-}
-
-.carousel-control-next {
- right: 0;
- @if $enable-gradients {
- background: linear-gradient(270deg, rgba($black, 0.25), rgba($black, 0.001));
- }
-}
-
-// Icons for within
-.carousel-control-prev-icon,
-.carousel-control-next-icon {
- display: inline-block;
- width: $carousel-control-icon-width;
- height: $carousel-control-icon-width;
- background: no-repeat 50% / 100% 100%;
-}
-
-.carousel-control-prev-icon {
- background-image: $carousel-control-prev-icon-bg;
-}
-
-.carousel-control-next-icon {
- background-image: $carousel-control-next-icon-bg;
-}
-
-
-// Optional indicator pips
-//
-// Add an ordered list with the following class and add a list item for each
-// slide your carousel holds.
-
-.carousel-indicators {
- position: absolute;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 15;
- display: flex;
- justify-content: center;
- padding-left: 0; // override <ol> default
- // Use the .carousel-control's width as margin so we don't overlay those
- margin-right: $carousel-control-width;
- margin-left: $carousel-control-width;
- list-style: none;
-
- li {
- box-sizing: content-box;
- flex: 0 1 auto;
- width: $carousel-indicator-width;
- height: $carousel-indicator-height;
- margin-right: $carousel-indicator-spacer;
- margin-left: $carousel-indicator-spacer;
- text-indent: -999px;
- cursor: pointer;
- background-color: $carousel-indicator-active-bg;
- background-clip: padding-box;
- // Use transparent borders to increase the hit area by 10px on top and bottom.
- border-top: $carousel-indicator-hit-area-height solid transparent;
- border-bottom: $carousel-indicator-hit-area-height solid transparent;
- opacity: 0.5;
- @include transition($carousel-indicator-transition);
- }
-
- .active {
- opacity: 1;
- }
-}
-
-
-// Optional captions
-//
-//
-
-.carousel-caption {
- position: absolute;
- right: (100% - $carousel-caption-width) / 2;
- bottom: 20px;
- left: (100% - $carousel-caption-width) / 2;
- z-index: 10;
- padding-top: 20px;
- padding-bottom: 20px;
- color: $carousel-caption-color;
- text-align: center;
-}
diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss
index 2204b037f69..95025459cc9 100644
--- a/app/assets/stylesheets/framework/ci_variable_list.scss
+++ b/app/assets/stylesheets/framework/ci_variable_list.scss
@@ -98,13 +98,3 @@
color: $gl-text-color-disabled;
}
}
-
-.group-variable-list {
- color: $gray-500;
-
- .table-section:not(:first-child) {
- @include media-breakpoint-down(sm) {
- border-top: hidden;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 3b59c028437..5d182373fb1 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -110,7 +110,7 @@ pre {
}
hr {
- margin: 24px 0;
+ margin: 1.5rem 0;
border-top: 1px solid $gray-darker;
}
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 745d469e3e8..c5467c304ec 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -471,7 +471,7 @@
background-color: $black-transparent;
height: 100%;
width: 100%;
- z-index: 300;
+ z-index: $zindex-dropdown-menu;
}
}
}
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 499b9c00116..e30caeb1dfb 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -1136,10 +1136,6 @@ table.code {
display: block;
}
}
-
- .note-edit-form {
- margin-left: $note-icon-gutter-width;
- }
}
.discussion-body .image .frame {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 41fc4d3dd4e..f56d8f2c2a9 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -216,7 +216,7 @@
position: absolute;
width: auto;
top: 100%;
- z-index: 300;
+ z-index: $zindex-dropdown-menu;
min-width: 240px;
max-width: 500px;
margin-top: $dropdown-vertical-offset;
@@ -326,7 +326,7 @@
color: $gl-text-color-secondary;
}
- .badge.badge-pill + span:not(.badge.badge-pill) {
+ .badge.badge-pill + span:not(.badge):not(.badge-pill) {
// Expects up to 3 digits on the badge
margin-right: 40px;
}
@@ -497,7 +497,7 @@
li {
a,
button,
- .dropdown-item {
+ .dropdown-item:not(.open-with-link) {
padding: 8px 40px;
position: relative;
@@ -525,7 +525,7 @@
&.is-active {
/* stylelint-disable-next-line function-url-quotes */
- background: url(asset_path('checkmark.png')) no-repeat 14px 8px;
+ background: url(asset_path('checkmark.png')) no-repeat 14px center;
}
}
}
@@ -896,7 +896,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
position: absolute;
top: 13px;
right: 25px;
- color: $gray-50;
+ color: $gray-300;
}
}
@@ -1093,17 +1093,3 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
width: $gl-dropdown-width-wide;
}
}
-
-.gl-dropdown-item-deprecated-adapter {
- .dropdown-item {
- align-items: flex-start;
-
- .gl-new-dropdown-item-text-primary {
- @include gl-font-weight-bold;
- }
-
- .gl-new-dropdown-item-text-secondary {
- color: inherit;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
index 8d411747b28..36f1b1f2903 100644
--- a/app/assets/stylesheets/framework/feature_highlight.scss
+++ b/app/assets/stylesheets/framework/feature_highlight.scss
@@ -1,14 +1,7 @@
.feature-highlight {
- position: relative;
- margin-left: $gl-padding;
- width: 20px;
- height: 20px;
- cursor: pointer;
-
&::before {
content: '';
display: block;
- position: absolute;
top: 6px;
left: 6px;
width: 8px;
@@ -29,56 +22,22 @@
}
}
-.feature-highlight-popover-content {
- display: none;
-
- hr {
- margin: $gl-padding 0;
- }
-
- .btn-link {
- svg {
- @include btn-svg;
-
- path {
- fill: currentColor;
- }
- }
- }
-
- .feature-highlight-illustration {
- width: 100%;
- height: 100px;
- padding-top: 12px;
- padding-bottom: 12px;
- background-color: $indigo-50;
- border-top-left-radius: 2px;
- border-top-right-radius: 2px;
- border-bottom: 1px solid darken($gray-normal, 8%);
- }
-}
-
-.popover .feature-highlight-popover-content {
- display: block;
+.feature-highlight-illustration {
+ background-color: $indigo-50;
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ border-bottom: 1px solid darken($gray-normal, 8%);
}
.feature-highlight-popover {
width: 240px;
- &.right > .arrow {
- border-right-color: $border-color;
- }
-
.popover-body {
padding: 0;
}
}
-.feature-highlight-popover-sub-content {
- padding: $gl-padding $gl-padding-12;
-}
-
@include keyframes(pulse-highlight) {
0% {
box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index fe8c27ae9b6..bda123fa7ea 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -2,6 +2,14 @@
* File content holder
*
*/
+.container-fluid.container-limited.limit-container-width {
+ .file-holder.readme-holder.limited-width-container .file-content {
+ max-width: $limited-layout-width;
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
.file-holder {
border: 1px solid $border-color;
border-top: 0;
@@ -17,12 +25,6 @@
&.readme-holder {
margin: $gl-padding 0;
-
- &.limited-width-container .file-content {
- max-width: $limited-layout-width;
- margin-left: auto;
- margin-right: auto;
- }
}
.file-title {
@@ -351,23 +353,17 @@ span.idiff {
color: $gl-text-color;
}
- .file-actions .btn:not(.btn-icon) {
- padding: 0 10px;
- font-size: 13px;
- line-height: 28px;
- display: inline-block;
- float: none;
- }
-
.file-actions .ide-edit-button {
z-index: 2;
}
- @include media-breakpoint-down(sm) {
- display: block;
-
+ @include media-breakpoint-down(md) {
.file-actions {
- margin-top: 5px;
+ margin-top: $gl-padding-8;
+
+ .btn {
+ margin-bottom: $gl-padding-8;
+ }
}
}
}
@@ -442,12 +438,6 @@ span.idiff {
.user-avatar-link.new-comment {
position: absolute;
margin: 40px $gl-padding 0 116px;
-
- ~ .note-edit-form form.edit-note {
- @include media-breakpoint-up(sm) {
- margin-left: $note-icon-gutter-width;
- }
- }
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 5f56fa3be86..07d59847829 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -475,6 +475,15 @@
}
}
+ .sort-dropdown-container {
+ // This property is set to have borders
+ // around sort dropdown match with filter
+ // input field.
+ .gl-button {
+ box-shadow: inset 0 0 0 1px $gray-400;
+ }
+ }
+
@include media-breakpoint-up(md) {
.sort-dropdown-container {
margin-left: 10px;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 730e10114c3..ffcc20b624b 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -516,28 +516,8 @@
left: auto;
max-height: $dropdown-max-height-lg;
- li.current-user {
- padding: $dropdown-item-padding-y $dropdown-item-padding-x;
-
- .user-name {
- display: block;
- }
-
- .user-status {
- margin-right: 0;
- max-width: 240px;
- font-size: $gl-font-size-small;
-
- gl-emoji {
- font-size: $gl-font-size-small;
- }
-
- .user-status-emoji {
- gl-emoji {
- font-size: $gl-font-size;
- }
- }
- }
+ .user-status {
+ max-width: 240px;
}
svg {
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 5623d38d66e..222e10f51ad 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -3,10 +3,6 @@
svg {
fill: $green-500;
}
-
- &.add-border {
- @include borderless-status-icon($green-500);
- }
}
.ci-status-icon-error,
@@ -14,10 +10,6 @@
svg {
fill: $red-500;
}
-
- &.add-border {
- @include borderless-status-icon($red-500);
- }
}
.ci-status-icon-pending,
@@ -27,59 +19,29 @@
svg {
fill: $orange-500;
}
-
- &.add-border {
- @include borderless-status-icon($orange-500);
- }
-}
-
-.ci-status-icon-preparing {
- svg {
- fill: $gray-300;
- }
-
- &.add-border {
- @include borderless-status-icon($gray-300);
- }
}
.ci-status-icon-running {
svg {
fill: $blue-400;
}
-
- &.add-border {
- @include borderless-status-icon($blue-400);
- }
}
.ci-status-icon-canceled,
-.ci-status-icon-disabled {
+.ci-status-icon-disabled,
+.ci-status-icon-scheduled,
+.ci-status-icon-manual {
svg {
fill: $gl-text-color;
}
-
- &.add-border {
- @include borderless-status-icon($gl-text-color);
- }
}
+.ci-status-icon-preparing,
.ci-status-icon-created,
.ci-status-icon-skipped,
.ci-status-icon-notfound {
svg {
- fill: $gray-darkest;
- }
-
- &.add-border {
- @include borderless-status-icon($gray-darkest);
- }
-}
-
-.ci-status-icon-scheduled,
-.ci-status-icon-manual {
- svg {
- fill: $gl-text-color;
+ fill: var(--gray-400, $gray-400);
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index e3d02d01496..e29e204b14f 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -354,13 +354,6 @@
}
}
-@mixin borderless-status-icon($color) {
- svg {
- border: 1px solid $color;
- border-radius: 50%;
- }
-}
-
@mixin emoji-menu-toggle-button {
line-height: 1;
padding: 0;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index bef33bd2ef0..241aaad015e 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -75,7 +75,7 @@
.right-sidebar-expanded {
padding-right: 0;
- z-index: 300;
+ z-index: $zindex-dropdown-menu;
@include media-breakpoint-only(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 89713fdbbea..92405c00c5e 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -69,9 +69,6 @@ table {
}
}
- td {
- border-color: $white-normal;
- }
}
.thead-white {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 1a568bb41a5..496e2aba421 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -458,7 +458,7 @@
h6 {
a.anchor {
float: left;
- margin-left: -16px;
+ margin-left: -20px;
text-decoration: none;
outline: none;
@@ -471,6 +471,11 @@
&:hover > a.anchor::after {
visibility: visible;
}
+
+ > a.anchor:focus::after {
+ visibility: visible;
+ outline: auto;
+ }
}
.big {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 674ba1a307b..4bf9236407f 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -433,6 +433,7 @@ $browser-scrollbar-size: 10px;
*/
$header-height: 40px;
$header-zindex: 1000;
+$zindex-dropdown-menu: 300;
$suggestion-header-height: 46px;
$ide-statusbar-height: 25px;
$fixed-layout-width: 1280px;
@@ -626,7 +627,6 @@ $search-input-xl-width: 320px;
$note-disabled-comment-color: #b2b2b2;
$note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3;
-$note-icon-gutter-width: 55px;
/*
* Identicon
@@ -871,6 +871,27 @@ $add-to-slack-well-max-width: 750px;
$add-to-slack-logo-size: 100px;
/*
+Security & Compliance Carousel
+*/
+$security-and-compliance-carousel-image-carousel-width: 1000px;
+$security-and-compliance-carousel-image-discover-button-width: 45%;
+$security-and-compliance-carousel-image-discover-buttons-max-width: 280px;
+$security-and-compliance-carousel-image-discover-footer-max-width: 500px;
+$security-and-compliance-carousel-image-discover-feedback-width: 30%;
+$security-and-compliance-carousel-image-discover-text-carousel-max-width: 650px;
+$security-and-compliance-carousel-image-discover-text-carousel-caption-height: 100%;
+$security-and-compliance-carousel-image-discover-text-carousel-caption-max-width: 500px;
+$security-and-compliance-carousel-control-icon-width: 10px;
+$security-and-compliance-carousel-control-position: -5%;
+$security-and-compliance-carousel-inner-width: 90%;
+$security-and-compliance-carousel-indicators-bottom: -20px;
+$security-and-compliance-carousel-indicators-bottom-lg: -15px;
+$security-and-compliance-carousel-indicators-dimension: 6px;
+$security-and-compliance-carousel-indicators-border-radius: 100%;
+$security-and-compliance-carousel-prev-icon-background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23666666' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E");
+$security-and-compliance-carousel-next-icon-background: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23666666' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E");
+
+/*
Popup
*/
$popup-triangle-size: 15px;
diff --git a/app/assets/stylesheets/mailer.scss b/app/assets/stylesheets/mailer.scss
index 27c6ef20269..4f76deeb991 100644
--- a/app/assets/stylesheets/mailer.scss
+++ b/app/assets/stylesheets/mailer.scss
@@ -14,13 +14,15 @@ $mailer-line-cell-bg-color: #6b4fbb;
$mailer-wrapper-cell-bg-color: #fff;
$mailer-wrapper-cell-border-color: #ededed;
$mailer-header-footer-text-color: #5c5c5c;
+$full-width: 640px;
+$half-width: 320px;
body {
margin: 0 !important;
background-color: $mailer-bg-color;
padding: 0;
text-align: center;
- min-width: 640px;
+ min-width: $full-width;
width: 100%;
height: 100%;
font-family: $mailer-font;
@@ -31,7 +33,7 @@ table#body {
margin: 0;
padding: 0;
text-align: center;
- min-width: 640px;
+ min-width: $full-width;
width: 100%;
}
@@ -44,6 +46,11 @@ a {
}
}
+.mail-avatar {
+ border-radius: 50%;
+ display: block;
+}
+
.highlight {
font-weight: 500;
}
@@ -77,10 +84,18 @@ a {
margin-left: 4px;
}
+.half-width {
+ min-width: $half-width;
+}
+
tr td {
font-family: $mailer-font;
}
+tr.border-top td {
+ border-top: 2px solid $gray-100;
+}
+
tr.line td {
font-family: $mailer-font;
background-color: $mailer-line-cell-bg-color;
@@ -100,7 +115,7 @@ td.footer-message {
}
table.wrapper {
- width: 640px;
+ width: $full-width;
margin: 0 auto;
border-collapse: separate;
border-spacing: 0;
@@ -149,7 +164,7 @@ tr.footer td {
}
.gitlab-info-text {
- max-width: 640px;
+ max-width: $full-width;
margin: 0 auto;
text-align: center;
color: $gray-400;
diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
index 093cba3560f..7336d555f79 100644
--- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -5,7 +5,7 @@
$bs-input-focus-border: #80bdff;
$bs-input-focus-box-shadow: rgba(0, 123, 255, 0.25);
- a:not(.btn):not(.gl-tab-nav-item),
+ a:not(.btn),
.gl-button.btn-link,
.gl-button.btn-link:hover,
.gl-button.btn-link:focus,
@@ -34,6 +34,7 @@
.ide-pipeline .top-bar .controllers .controllers-buttons,
.controllers-buttons svg,
.nav-links li a.active,
+ .gl-tabs-nav li a.gl-tab-nav-item-active,
.md-area.is-focused {
color: var(--ide-text-color, $gl-text-color);
}
@@ -44,13 +45,15 @@
}
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a,
+ .gl-tabs-nav li a,
.dropdown-menu-inner-content,
.file-row .file-row-icon svg,
.file-row:hover .file-row-icon svg {
color: var(--ide-text-color-secondary, $gl-text-color-secondary);
}
- .nav-links:not(.quick-links) li:not(.md-header-toolbar) {
+ .nav-links:not(.quick-links) li:not(.md-header-toolbar),
+ .gl-tabs-nav li {
&:hover a,
&.active a,
a:hover,
@@ -61,6 +64,10 @@
border-color: var(--ide-input-border, $gray-darkest);
}
}
+
+ a.gl-tab-nav-item-active {
+ box-shadow: inset 0 -2px 0 0 var(--ide-input-border, $gray-darkest);
+ }
}
.drag-handle:hover {
@@ -142,6 +149,7 @@
.md table:not(.code) tbody td,
.md table:not(.code) tr th,
.nav-links:not(.quick-links),
+ .gl-tabs-nav,
.common-note-form .md-area.is-focused .nav-links {
border-color: var(--ide-border-color-alt, $white-dark);
}
@@ -175,6 +183,10 @@
border-color: var(--ide-highlight-accent, $gl-text-color);
}
+ .gl-tabs-nav li a.gl-tab-nav-item-active {
+ box-shadow: inset 0 -2px 0 0 var(--ide-highlight-accent, $gl-text-color);
+ }
+
// for other themes, suppress different avatar default colors for simplicity
.avatar-container {
&,
@@ -241,7 +253,6 @@
.btn-default:not(.gl-button),
.dropdown,
.dropdown-menu-toggle {
- background-color: var(--ide-input-background, $white) !important;
color: var(--ide-input-color, $gl-text-color) !important;
border-color: var(--ide-btn-default-border, $border-color);
}
@@ -304,6 +315,11 @@
border-color: var(--ide-dropdown-hover-background, $border-color);
}
+ .gl-tabs-nav {
+ background-color: var(--ide-dropdown-hover-background, $white);
+ box-shadow: inset 0 -2px 0 0 var(--ide-dropdown-hover-background, $border-color);
+ }
+
.divider {
background-color: var(--ide-dropdown-hover-background, $gray-100);
border-color: var(--ide-dropdown-hover-background, $gray-100);
diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
index 72e2a45565e..0d6f360112b 100644
--- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
@@ -98,7 +98,6 @@
@include mini-pipeline-graph-color(var(--white, $white), $orange-50, $orange-100, $orange-500, $orange-600, $orange-700);
}
- &.ci-status-icon-preparing,
&.ci-status-icon-running {
@include mini-pipeline-graph-color(var(--white, $white), $blue-100, $blue-200, $blue-500, $blue-600, $blue-700);
}
@@ -106,14 +105,15 @@
&.ci-status-icon-canceled,
&.ci-status-icon-scheduled,
&.ci-status-icon-disabled,
- &.ci-status-icon-not-found,
&.ci-status-icon-manual {
@include mini-pipeline-graph-color(var(--white, $white), $gray-500, $gray-700, $gray-900, $gray-950, $black);
}
+ &.ci-status-icon-preparing,
&.ci-status-icon-created,
+ &.ci-status-icon-not-found,
&.ci-status-icon-skipped {
- @include mini-pipeline-graph-color(var(--white, $white), $gray-100, $gray-200, $gray-300, $gray-400, $gray-500);
+ @include mini-pipeline-graph-color(var(--white, $white), var(--gray-100, $gray-100), var(--gray-200, $gray-200), var(--gray-400, $gray-400), var(--gray-500, $gray-500), var(--gray-600, $gray-600));
}
}
diff --git a/app/assets/stylesheets/page_bundles/admin/application_settings_metrics_and_profiling.scss b/app/assets/stylesheets/page_bundles/admin/application_settings_metrics_and_profiling.scss
new file mode 100644
index 00000000000..41bb6d107f1
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/admin/application_settings_metrics_and_profiling.scss
@@ -0,0 +1,3 @@
+.usage-data {
+ max-height: 400px;
+}
diff --git a/app/assets/stylesheets/page_bundles/admin/jobs_index.scss b/app/assets/stylesheets/page_bundles/admin/jobs_index.scss
new file mode 100644
index 00000000000..7844cae5f87
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/admin/jobs_index.scss
@@ -0,0 +1,5 @@
+.admin-builds-table {
+ td:last-child {
+ min-width: 120px;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 3d1ae3519a9..620b37914d8 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -138,6 +138,47 @@
border: 1px solid var(--gray-100, $gray-100);
}
+// to highlight columns we have animated pulse of box-shadow
+// we don't want to actually animate the box-shadow property
+// because that causes costly repaints. Instead we can add a
+// pseudo-element that is the same size as our element, then
+// animate opacity/transform to give a soothing single pulse
+.board-column-highlighted::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ pointer-events: none;
+ opacity: 0;
+ z-index: -1;
+ box-shadow: 0 0 6px 3px $blue-200;
+ animation-name: board-column-flash-border;
+ animation-duration: 1.2s;
+ animation-fill-mode: forwards;
+ animation-timing-function: ease-in-out;
+}
+
+@keyframes board-column-flash-border {
+ 0%,
+ 100% {
+ opacity: 0;
+ transform: scale(0.98);
+ }
+
+ 25%,
+ 75% {
+ opacity: 1;
+ transform: scale(0.99);
+ }
+
+ 50% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
.board-header {
&.has-border::before {
border-top: 3px solid;
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index f6b9473d235..7c4d51ab677 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -97,7 +97,8 @@ $ide-commit-header-height: 48px;
border-right: 1px solid var(--ide-border-color, $white-dark);
border-bottom: 1px solid var(--ide-border-color, $white-dark);
- &.active {
+ &.active,
+ .gl-tab-nav-item-active {
background-color: var(--ide-highlight-background, $white);
border-bottom-color: transparent;
}
@@ -114,6 +115,42 @@ $ide-commit-header-height: 48px;
}
}
}
+
+ .gl-tab-content {
+ padding: 0;
+ }
+
+ .gl-tabs-nav {
+ border-width: 0;
+
+ li {
+ padding: 0 !important;
+ background: transparent !important;
+ border: 0 !important;
+
+ a {
+ display: flex;
+ align-items: center;
+ padding: $grid-size $gl-padding !important;
+ box-shadow: none !important;
+ font-weight: normal !important;
+
+ background-color: var(--ide-background-hover, $gray-normal);
+ border-right: 1px solid var(--ide-border-color, $white-dark);
+ border-bottom: 1px solid var(--ide-border-color, $white-dark);
+
+ &.gl-tab-nav-item-active {
+ background-color: var(--ide-highlight-background, $white);
+ border-color: var(--ide-border-color, $white-dark);
+ border-bottom-color: transparent;
+ }
+
+ .multi-file-tab-close svg {
+ top: 0;
+ }
+ }
+ }
+ }
}
.multi-file-tab {
@@ -605,6 +642,17 @@ $ide-commit-header-height: 48px;
left: -1px;
}
}
+
+ .ide-commit-badge {
+ background-color: var(--ide-highlight-accent, $almost-black) !important;
+ color: var(--ide-highlight-background, $white) !important;
+ position: absolute;
+ left: 38px;
+ top: $gl-padding-8;
+ font-size: $gl-font-size-12;
+ padding: 2px $gl-padding-4;
+ font-weight: $gl-font-weight-bold !important;
+ }
}
.ide-activity-bar {
@@ -623,7 +671,8 @@ $ide-commit-header-height: 48px;
height: 100%;
}
- .nav-links {
+ .nav-links,
+ .gl-tabs-nav {
height: 30px;
}
@@ -965,17 +1014,25 @@ $ide-commit-header-height: 48px;
}
.ide-nav-form {
- .nav-links li {
+ .nav-links li,
+ .gl-tabs-nav li {
width: 50%;
padding-left: 0;
padding-right: 0;
a {
text-align: center;
+ font-size: 14px;
+ line-height: 30px;
- &:not(.active) {
+ &:not(.active),
+ &:not(.gl-tab-nav-item-active) {
background-color: var(--ide-dropdown-background, $gray-light);
}
+
+ &.gl-tab-nav-item-active {
+ font-weight: bold;
+ }
}
}
diff --git a/app/assets/stylesheets/page_bundles/import.scss b/app/assets/stylesheets/page_bundles/import.scss
index 5f43d5df7e3..453b810196b 100644
--- a/app/assets/stylesheets/page_bundles/import.scss
+++ b/app/assets/stylesheets/page_bundles/import.scss
@@ -12,35 +12,6 @@
width: 1%;
}
-.import-project-name-input {
- border-radius: 0 $border-radius-default $border-radius-default 0;
- position: relative;
- left: -1px;
- max-width: 300px;
-}
-
-.import-slash-divider {
- background-color: $gray-lightest;
- border: 1px solid $border-color;
-}
-
-.import-row {
- height: 55px;
-}
-
-.import-table {
- .import-jobs-from-col,
- .import-jobs-to-col,
- .import-jobs-status-col,
- .import-jobs-cta-col {
- border-bottom-width: 1px;
- padding-left: $gl-padding;
- }
-}
-
-.import-projects-loading-icon {
- margin-top: $gl-padding-32;
-}
.import-entities-target-select {
&.disabled {
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
index 231723ca4e3..25401a161da 100644
--- a/app/assets/stylesheets/page_bundles/jira_connect.scss
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -79,12 +79,6 @@ $header-height: 40px;
margin-top: 16px;
}
-.heading-with-border {
- border-bottom: 1px solid $gray-100;
- display: inline-block;
- padding-bottom: 16px;
-}
-
svg {
fill: currentColor;
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index be74503c21f..3263a5067ea 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -7,7 +7,6 @@
.diff-files-holder {
flex: 1;
min-width: 0;
- z-index: 201;
}
.diff-tree-list {
diff --git a/app/assets/stylesheets/page_bundles/oncall_schedules.scss b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
index 1b190024457..a6668f00147 100644
--- a/app/assets/stylesheets/page_bundles/oncall_schedules.scss
+++ b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
@@ -37,12 +37,6 @@
&.gl-modal .modal-md {
max-width: 640px;
}
-
- // TODO: move to gitlab/ui utilities
- // https://gitlab.com/gitlab-org/gitlab/-/issues/297502
- .gl-w-fit-content {
- width: fit-content;
- }
}
//// Copied from roadmaps.scss - adapted for on-call schedules
@@ -91,9 +85,6 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi
}
.timeline-header-item {
- // container size minus left panel width divided by 2 week timeframes
- width: calc((100% - #{$details-cell-width}) / 2);
-
&:last-of-type .item-label {
@include gl-border-r-0;
}
@@ -174,9 +165,6 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi
.timeline-cell {
@include gl-relative;
- // width: $timeline-cell-width;
- // container size minus left panel width divided by 2 week timeframes
- width: calc((100% - #{$details-cell-width}) / 2);
@include gl-bg-transparent;
border-right: $border-style;
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index dbde7933a8b..ae36f7e3ac1 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -67,7 +67,8 @@
// Mini Pipelines
.stage-cell {
- .mini-pipeline-graph-dropdown-toggle {
+ .mini-pipeline-graph-dropdown-toggle,
+ .mini-pipeline-graph-gl-dropdown-toggle {
svg {
height: $ci-action-icon-size;
width: $ci-action-icon-size;
@@ -138,7 +139,13 @@
}
// Dropdown button in mini pipeline graph
-button.mini-pipeline-graph-dropdown-toggle {
+button.mini-pipeline-graph-dropdown-toggle,
+// As the `mini-pipeline-item` mixin specificity is lower
+// than the toggle of dropdown with 'variant="link"' we add
+// classes ".gl-button.btn-link" to make it more specific.
+// Once FF ci_mini_pipeline_gl_dropdown is removed, the `mini-pipeline-item`
+// itself could increase its specificity to simplify this selector
+button.gl-button.btn-link.mini-pipeline-graph-gl-dropdown-toggle {
@include mini-pipeline-item();
}
diff --git a/app/assets/stylesheets/page_bundles/signup.scss b/app/assets/stylesheets/page_bundles/signup.scss
index 9ed48b693b9..a207c10b04f 100644
--- a/app/assets/stylesheets/page_bundles/signup.scss
+++ b/app/assets/stylesheets/page_bundles/signup.scss
@@ -73,3 +73,7 @@
text-decoration: none;
}
}
+
+.edit-profile {
+ max-width: 460px;
+}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
deleted file mode 100644
index af1eefd7587..00000000000
--- a/app/assets/stylesheets/pages/admin.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-.info-well {
- .admin-well-statistics,
- .admin-well-features {
- padding-bottom: 46px;
- }
-}
-
-.usage-data {
- max-height: 400px;
-}
-
-[data-page='admin:jobs:index'] {
- .admin-builds-table {
- td:last-child {
- min-width: 120px;
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 9b17da80023..7111d3d4107 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -240,11 +240,6 @@
.commit,
.generic-commit-status {
- a,
- button {
- vertical-align: baseline;
- }
-
a {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 52bd16d1a79..ef737e11799 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -74,14 +74,10 @@
justify-content: flex-end;
}
- .encoding-selector,
.soft-wrap-toggle {
display: inline-block;
vertical-align: top;
font-family: $regular-font;
- }
-
- .soft-wrap-toggle {
margin: 0 $btn-side-margin;
.soft-wrap {
@@ -128,15 +124,6 @@
margin: 3px 0;
}
- .encoding-selector {
- display: block;
- margin: 3px 0;
-
- button {
- width: 100%;
- }
- }
-
@media(max-width: map-get($grid-breakpoints, md)-1) {
clear: both;
}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index aeda91c1714..87307fd682e 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -260,7 +260,6 @@
}
.pipeline-quota {
- border-top: 1px solid $table-border-color;
border-bottom: 1px solid $table-border-color;
margin: 0 0 $gl-padding;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index e5528c25e82..a6ab5459a84 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -117,6 +117,11 @@
}
}
+.reviewer .merge-icon {
+ bottom: -3px;
+ right: -3px;
+}
+
.right-sidebar {
position: fixed;
top: $header-height;
@@ -156,14 +161,6 @@
color: inherit;
}
- // TODO remove this class once we can generate a correct hover utility from `gitlab/ui`,
- // see here: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39286#note_396767000
- .btn-link-hover:hover {
- * {
- @include gl-text-blue-800;
- }
- }
-
.issuable-header-text {
margin-top: 7px;
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 1caf62067a6..2a8a86615f6 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -108,18 +108,6 @@ ul.related-merge-requests > li {
}
}
-.issuable-email-modal-btn {
- padding: 0;
- color: $blue-600;
- background-color: transparent;
- border: 0;
- outline: 0;
-
- &:hover {
- text-decoration: underline;
- }
-}
-
.email-modal-input-group {
margin-bottom: 10px;
@@ -203,15 +191,9 @@ ul.related-merge-requests > li {
}
}
-.discussion-reply-holder {
- .avatar-note-form-holder .note-edit-form {
- display: block;
- margin-left: $note-icon-gutter-width;
-
- @include media-breakpoint-down(xs) {
- margin-left: 0;
- }
- }
+.discussion-reply-holder,
+.note-edit-form {
+ display: block;
}
.issue-sort-dropdown {
@@ -221,6 +203,10 @@ ul.related-merge-requests > li {
.reverse-sort-btn {
color: $gl-text-color-secondary;
+
+ &.disabled {
+ color: $gl-text-color-disabled;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 4d93702f1c2..b7d05fc411a 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -120,7 +120,7 @@
}
.labels-container {
- background-color: $gray-light;
+ background-color: $gray-100;
border-radius: $border-radius-default;
padding: $gl-padding $gl-padding-8;
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 81a70470c65..019d827798c 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -42,8 +42,7 @@
.login-box,
.omniauth-container {
box-shadow: 0 0 0 1px $border-color;
- border-bottom-right-radius: $border-radius;
- border-bottom-left-radius: $border-radius;
+ border-radius: $border-radius;
padding: 15px;
.login-heading h3 {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index b99e619cc98..a62df1258af 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -221,7 +221,7 @@ $mr-widget-min-height: 69px;
.mr-widget-pipeline-graph {
.dropdown-menu {
- z-index: 300;
+ z-index: $zindex-dropdown-menu;
}
}
@@ -375,13 +375,14 @@ $mr-widget-min-height: 69px;
}
.text {
- span {
- font-weight: $gl-font-weight-bold;
- }
-
p {
margin-top: $gl-padding;
}
+
+ .highlight {
+ margin: 0 0 $gl-padding;
+ font-weight: $gl-font-weight-bold;
+ }
}
}
@@ -981,7 +982,14 @@ $mr-widget-min-height: 69px;
}
.mini-pipeline-graph-dropdown-toggle,
- .stage-cell .mini-pipeline-graph-dropdown-toggle svg {
+ .stage-cell .mini-pipeline-graph-dropdown-toggle svg,
+ // As the `mini-pipeline-item` mixin specificity is lower
+ // than the toggle of dropdown with 'variant="link"' we add
+ // classes ".gl-button.btn-link" to make it more specific.
+ // Once FF ci_mini_pipeline_gl_dropdown is removed, the `mini-pipeline-item`
+ // itself could increase its specificity to simplify this selector
+ button.gl-button.btn-link.mini-pipeline-graph-gl-dropdown-toggle,
+ .stage-cell button.gl-button.btn-link.mini-pipeline-graph-gl-dropdown-toggle svg {
height: $ci-action-icon-size-lg;
width: $ci-action-icon-size-lg;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 254ad96bb57..ffbfa47f9bd 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -212,8 +212,12 @@ table {
}
}
-.note-edit-form {
+// Snippets are the only non-vue form left
+.snippets.note-edit-form {
display: none;
+}
+
+.note-edit-form {
font-size: 14px;
.md-area {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 4216091e8a9..190bdcb1efd 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -1,6 +1,5 @@
$system-note-icon-size: 32px;
$system-note-svg-size: 16px;
-$note-form-margin-left: 72px;
@mixin vertical-line($left) {
&::before {
@@ -54,16 +53,6 @@ $note-form-margin-left: 72px;
&.note-form {
margin-left: 0;
- @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;
}
@@ -84,36 +73,17 @@ $note-form-margin-left: 72px;
.replies-toggle {
background-color: $gray-light;
padding: $gl-padding-8 $gl-padding;
- border-top: 1px solid $gray-50;
- border-bottom: 1px solid $gray-50;
+ border-top: 1px solid $gray-100;
+ border-bottom: 1px solid $gray-100;
.collapse-replies-btn:hover {
color: $blue-600;
}
- &.expanded {
- span {
- cursor: pointer;
- }
-
- svg {
- position: relative;
- top: 3px;
- }
- }
-
&.collapsed {
color: $gl-text-color-secondary;
border-radius: 0 0 $border-radius-default $border-radius-default;
- svg {
- float: left;
- position: relative;
- top: $gl-padding-4;
- margin-right: $gl-padding-8;
- cursor: pointer;
- }
-
img {
margin: -2px 4px 0 0;
}
@@ -178,7 +148,6 @@ $note-form-margin-left: 72px;
> li {
display: block;
position: relative;
- border-bottom: 0;
&.being-posted {
pointer-events: none;
@@ -549,21 +518,6 @@ $note-form-margin-left: 72px;
.code-commit .notes-content,
.diff-viewer > .image ~ .note-container {
background-color: $white;
-
- .avatar-note-form-holder {
- .user-avatar-link img {
- margin: 13px $gl-padding $gl-padding;
- }
-
- form,
- ~ .discussion-form-container {
- padding: $gl-padding;
-
- @include media-breakpoint-up(sm) {
- margin-left: $note-icon-gutter-width;
- }
- }
- }
}
.diff-viewer > .image ~ .note-container form.new-note {
@@ -953,14 +907,6 @@ $note-form-margin-left: 72px;
.discussion-filter-container {
.dropdown-menu {
margin-bottom: $gl-padding-4;
-
- @include media-breakpoint-down(md) {
- margin-left: $btn-side-margin + $contextual-sidebar-collapsed-width;
- }
-
- @include media-breakpoint-down(xs) {
- margin-left: $btn-side-margin;
- }
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 3605283245f..6a2fa2ee7a1 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -138,6 +138,13 @@
}
}
+.social-provider-btn-image {
+ > img {
+ width: 16px;
+ vertical-align: inherit;
+ }
+}
+
.provider-btn-image {
display: inline-block;
padding: 5px 10px;
@@ -378,19 +385,6 @@ table.u2f-registrations,
display: inline;
margin-right: $gl-padding / 4;
}
-
- .badge-verification-status {
- border-width: 1px;
- border-style: solid;
-
- &.verified {
- @include green-status-color;
- }
-
- &.unverified {
- @include status-color($gray-dark, color('gray'), $common-gray-dark);
- }
- }
}
.edit-user {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 7fafd28be56..8251cdb9bbb 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -571,10 +571,6 @@
top: 0;
}
}
-
- .btn-missing {
- @extend .btn-missing;
- }
}
}
@@ -996,6 +992,20 @@ pre.light-well {
width: auto;
}
}
+
+ // Remove once gitlab/ui solution is implemented:
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1157
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/300405
+ .gl-search-box-by-type-input {
+ width: 100%;
+ }
+
+ // Remove once gitlab/ui solution is implemented
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1158
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/300405
+ .gl-new-dropdown-button-text {
+ @include str-truncated;
+ }
}
.clearable-input {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 352050f7b01..fa008a05e11 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -49,8 +49,6 @@
position: relative;
.dropdown-menu {
- min-width: 100%;
- width: 100%;
left: inherit;
right: 0;
}
@@ -114,7 +112,7 @@
}
th {
- border-top-color: $gray-light;
+ border: 0;
}
td {
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index 31a501f3a36..849749ee7c7 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -903,7 +903,7 @@ table a code {
padding: 0;
background-color: #4f4f4f;
}
-.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
+.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
margin-right: 40px;
}
.dropdown-select {
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 7f6e537af8f..44da509481d 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -736,7 +736,6 @@ body {
white-space: nowrap;
}
.btn:active, .btn.active {
- box-shadow: rgba(0, 0, 0, 0.16);
background-color: #eaeaea;
border-color: #e3e3e3;
color: #303030;
@@ -903,7 +902,7 @@ table a code {
padding: 0;
background-color: #dbdbdb;
}
-.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
+.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
margin-right: 40px;
}
.dropdown-select {
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index af43c532b7c..4b88b94f3a6 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -1174,7 +1174,7 @@ table a code {
padding: 0;
background-color: #dbdbdb;
}
-.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
+.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
margin-right: 40px;
}
.dropdown-select {
@@ -2093,8 +2093,7 @@ table.code {
.login-page .login-box,
.login-page .omniauth-container {
box-shadow: 0 0 0 1px #dbdbdb;
- border-bottom-right-radius: 0.25rem;
- border-bottom-left-radius: 0.25rem;
+ border-radius: 0.25rem;
padding: 15px;
}
.login-page .login-box .nav .active a,
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index ab330ed69c6..d424dcbf8f2 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -110,7 +110,7 @@
// This utility is used to force the z-index to match that of dropdown menu's
.gl-z-dropdown-menu\! {
- z-index: 300 !important;
+ z-index: $zindex-dropdown-menu !important;
}
.gl-flex-basis-quarter {
@@ -143,3 +143,17 @@
flex-direction: column !important;
}
}
+
+// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1165
+.gl-xs-mb-4 {
+ @media (max-width: $breakpoint-sm) {
+ margin-bottom: $gl-spacing-scale-4;
+ }
+}
+
+// Same as above
+.gl-xs-mb-4\! {
+ @media (max-width: $breakpoint-sm) {
+ margin-bottom: $gl-spacing-scale-4 !important;
+ }
+}