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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_actions.vue15
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/user_details.vue3
-rw-r--r--app/assets/javascripts/admin/abuse_report/constants.js6
-rw-r--r--app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue6
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue2
-rw-r--r--app/assets/javascripts/alert.js54
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_empty_state.vue6
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/base.vue4
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue1
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue124
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/actions.js4
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutations.js3
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/state.js1
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/devops_score.vue1
-rw-r--r--app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue6
-rw-r--r--app/assets/javascripts/analytics/shared/components/date_ranges_dropdown.vue131
-rw-r--r--app/assets/javascripts/analytics/shared/components/daterange.vue23
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue24
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js69
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/badges/components/badge.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue4
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_dropdown.vue8
-rw-r--r--app/assets/javascripts/batch_comments/components/review_bar.vue2
-rw-r--r--app/assets/javascripts/batch_comments/components/submit_dropdown.vue6
-rw-r--r--app/assets/javascripts/behaviors/autosize.js11
-rw-r--r--app/assets/javascripts/behaviors/components/global_alerts.vue50
-rw-r--r--app/assets/javascripts/behaviors/global_alerts.js17
-rw-r--r--app/assets/javascripts/behaviors/index.js3
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js14
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js45
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_observability.js25
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js20
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_edit_header.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_header_default_actions.vue5
-rw-r--r--app/assets/javascripts/blob/components/blob_header_filepath.vue2
-rw-r--r--app/assets/javascripts/blob/csv/constants.js1
-rw-r--r--app/assets/javascripts/blob/csv/csv_viewer.vue35
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue43
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue12
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue30
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue11
-rw-r--r--app/assets/javascripts/boards/components/board_top_bar.vue3
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue272
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue12
-rw-r--r--app/assets/javascripts/boards/components/toggle_focus.vue2
-rw-r--r--app/assets/javascripts/boards/graphql/cache_updates.js4
-rw-r--r--app/assets/javascripts/boards/graphql/client/selected_board_items.query.graphql3
-rw-r--r--app/assets/javascripts/boards/graphql/client/set_selected_board_items.mutation.graphql3
-rw-r--r--app/assets/javascripts/boards/graphql/client/unset_selected_board_items.mutation.graphql3
-rw-r--r--app/assets/javascripts/boards/issue_board_filters.js14
-rw-r--r--app/assets/javascripts/boards/stores/actions.js6
-rw-r--r--app/assets/javascripts/branches/components/delete_merged_branches.vue19
-rw-r--r--app/assets/javascripts/branches/components/sort_dropdown.vue4
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue17
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/constants.js3
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql1
-rw-r--r--app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue1
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue67
-rw-r--r--app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue8
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue120
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue103
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue41
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue130
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue13
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue55
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue59
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue57
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue74
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue144
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/empty_state.vue22
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue109
-rw-r--r--app/assets/javascripts/ci/catalog/constants.js35
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql25
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql20
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql29
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql6
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql7
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/settings.js32
-rw-r--r--app/assets/javascripts/ci/catalog/router/constants.js2
-rw-r--r--app/assets/javascripts/ci/catalog/router/index.js13
-rw-r--r--app/assets/javascripts/ci/catalog/router/routes.js9
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue43
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue368
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue19
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue10
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue5
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/utils.js23
-rw-r--r--app/assets/javascripts/ci/common/pipelines_table.vue110
-rw-r--r--app/assets/javascripts/ci/common/private/job_action_component.vue2
-rw-r--r--app/assets/javascripts/ci/constants.js15
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue4
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql1
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_header.vue43
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_log_controllers.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line.vue8
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue4
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_number.vue5
-rw-r--r--app/assets/javascripts/ci/job_details/components/manual_variables_form.vue8
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue14
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue6
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue34
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue4
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue141
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue6
-rw-r--r--app/assets/javascripts/ci/job_details/components/stuck_block.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/job_details/index.js32
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue33
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js6
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutations.js6
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js83
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue6
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue87
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue29
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue (renamed from app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue)23
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table.vue36
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue1
-rw-r--r--app/assets/javascripts/ci/jobs_page/constants.js15
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/constants.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/dag.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue22
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue14
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue248
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js11
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipelines_index.js30
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue11
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue24
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue15
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue85
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue26
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/constants.js1
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql18
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue1
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue20
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue33
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue59
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue (renamed from app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue)16
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue31
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue19
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue3
-rw-r--r--app/assets/javascripts/ci/pipelines_page/pipelines.vue81
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/utils.js45
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_details.vue15
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_form_fields.vue10
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_header.vue3
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_type_icon.vue62
-rw-r--r--app/assets/javascripts/ci/runner/constants.js50
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql4
-rw-r--r--app/assets/javascripts/ci/runner/sentry_utils.js17
-rw-r--r--app/assets/javascripts/ci/utils.js16
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_empty_state.vue6
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_empty_state.vue6
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js7
-rw-r--r--app/assets/javascripts/comment_templates/components/form.vue2
-rw-r--r--app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue18
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js2
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue16
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_provider.vue8
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue2
-rw-r--r--app/assets/javascripts/content_editor/extensions/selection.js12
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js9
-rw-r--r--app/assets/javascripts/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql (renamed from app/assets/javascripts/crm/organizations/components/graphql/create_organization.mutation.graphql)2
-rw-r--r--app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue4
-rw-r--r--app/assets/javascripts/custom_emoji/components/app.vue2
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_description/description_form.vue3
-rw-r--r--app/assets/javascripts/diffs/components/app.vue77
-rw-r--r--app/assets/javascripts/diffs/components/compare_dropdown_layout.vue2
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue24
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue17
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue2
-rw-r--r--app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql46
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue7
-rw-r--r--app/assets/javascripts/diffs/index.js5
-rw-r--r--app/assets/javascripts/diffs/store/actions.js6
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js5
-rw-r--r--app/assets/javascripts/diffs/store/utils.js15
-rw-r--r--app/assets/javascripts/diffs/utils/diff_file.js1
-rw-r--r--app/assets/javascripts/diffs/utils/merge_request.js7
-rw-r--r--app/assets/javascripts/diffs/utils/sort_findings_by_file.js17
-rw-r--r--app/assets/javascripts/editor/schema/ci.json147
-rw-r--r--app/assets/javascripts/editor/source_editor.js5
-rw-r--r--app/assets/javascripts/emoji/awards_app/index.js8
-rw-r--r--app/assets/javascripts/emoji/components/picker.vue13
-rw-r--r--app/assets/javascripts/environments/components/canary_ingress.vue63
-rw-r--r--app/assets/javascripts/environments/components/empty_state.vue6
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue11
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue8
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_pods.vue2
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_summary.vue2
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_tabs.vue4
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue2
-rw-r--r--app/assets/javascripts/environments/environment_details/deployments_table.vue7
-rw-r--r--app/assets/javascripts/environments/environment_details/index.vue8
-rw-r--r--app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/flux.js4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/kubernetes.js81
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue6
-rw-r--r--app/assets/javascripts/feature_flags/components/empty_state.vue1
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js50
-rw-r--r--app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue1
-rw-r--r--app/assets/javascripts/google_tag_manager/index.js311
-rw-r--r--app/assets/javascripts/graphql_shared/client/is_showing_labels.query.graphql3
-rw-r--r--app/assets/javascripts/graphql_shared/client/set_is_showing_labels.mutation.graphql3
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js58
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json3
-rw-r--r--app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql15
-rw-r--r--app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue1
-rw-r--r--app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue6
-rw-r--r--app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue1
-rw-r--r--app/assets/javascripts/groups/components/group_name_and_path.vue6
-rw-r--r--app/assets/javascripts/groups/components/transfer_group_form.vue2
-rw-r--r--app/assets/javascripts/helpers/startup_css_helper.js36
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue1
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js17
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/handle_tracking_event.js2
-rw-r--r--app/assets/javascripts/import/constants.js4
-rw-r--r--app/assets/javascripts/import_entities/components/group_dropdown.vue72
-rw-r--r--app/assets/javascripts/import_entities/components/import_target_dropdown.vue54
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue15
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue57
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue1
-rw-r--r--app/assets/javascripts/integrations/constants.js8
-rw-r--r--app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue2
-rw-r--r--app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue65
-rw-r--r--app/assets/javascripts/invite_members/components/invite_groups_modal.vue2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue16
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue1
-rw-r--r--app/assets/javascripts/issuable/components/hidden_badge.vue36
-rw-r--r--app/assets/javascripts/issuable/components/locked_badge.vue36
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue2
-rw-r--r--app/assets/javascripts/issuable/issuable_label_selector.js2
-rw-r--r--app/assets/javascripts/issues/constants.js2
-rw-r--r--app/assets/javascripts/issues/index.js11
-rw-r--r--app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue4
-rw-r--r--app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue21
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue15
-rw-r--r--app/assets/javascripts/issues/list/queries/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue1
-rw-r--r--app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue4
-rw-r--r--app/assets/javascripts/issues/service_desk/filtered_search_service_desk.js (renamed from app/assets/javascripts/issues/filtered_search_service_desk.js)0
-rw-r--r--app/assets/javascripts/issues/service_desk/index.js11
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue6
-rw-r--r--app/assets/javascripts/issues/show/components/delete_issue_modal.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue18
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue15
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue36
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/new_header_actions_popover.vue80
-rw-r--r--app/assets/javascripts/issues/show/components/sticky_header.vue45
-rw-r--r--app/assets/javascripts/issues/show/constants.js2
-rw-r--r--app/assets/javascripts/issues/show/index.js4
-rw-r--r--app/assets/javascripts/jira_connect/branches/pages/index.vue1
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js8
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_progress.vue1
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_setup.vue1
-rw-r--r--app/assets/javascripts/labels/index.js4
-rw-r--r--app/assets/javascripts/labels/label_manager.js2
-rw-r--r--app/assets/javascripts/lazy_loader.js2
-rw-r--r--app/assets/javascripts/lib/utils/global_alerts.js37
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js22
-rw-r--r--app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue2
-rw-r--r--app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue1
-rw-r--r--app/assets/javascripts/members/components/app.vue5
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue3
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js16
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_app.vue4
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_dropdown.vue1
-rw-r--r--app/assets/javascripts/merge_requests/components/header_metadata.vue69
-rw-r--r--app/assets/javascripts/merge_requests/components/merge_request_header.vue113
-rw-r--r--app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue74
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue33
-rw-r--r--app/assets/javascripts/merge_requests/index.js19
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue6
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue167
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js7
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue1
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue1
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index.js3
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue16
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue27
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue35
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/translations.js17
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js1
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue8
-rw-r--r--app/assets/javascripts/notes/components/comment_type_dropdown.vue5
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue9
-rw-r--r--app/assets/javascripts/notes/components/discussion_actions.vue3
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue7
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/email_participants_warning.vue7
-rw-r--r--app/assets/javascripts/notes/components/multiline_comment_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue23
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue7
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue8
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue9
-rw-r--r--app/assets/javascripts/notes/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/actions.js5
-rw-r--r--app/assets/javascripts/notes/stores/getters.js7
-rw-r--r--app/assets/javascripts/observability/client.js64
-rw-r--r--app/assets/javascripts/observability/components/observability_app.vue87
-rw-r--r--app/assets/javascripts/observability/components/observability_container.vue12
-rw-r--r--app/assets/javascripts/observability/components/skeleton/dashboards.vue30
-rw-r--r--app/assets/javascripts/observability/components/skeleton/embed.vue16
-rw-r--r--app/assets/javascripts/observability/components/skeleton/explore.vue28
-rw-r--r--app/assets/javascripts/observability/components/skeleton/index.vue27
-rw-r--r--app/assets/javascripts/observability/components/skeleton/manage.vue26
-rw-r--r--app/assets/javascripts/observability/constants.js28
-rw-r--r--app/assets/javascripts/observability/index.js60
-rw-r--r--app/assets/javascripts/observability/mock_traces.json107
-rw-r--r--app/assets/javascripts/organizations/index/components/app.vue61
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_list.vue26
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_list_item.vue54
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_view.vue52
-rw-r--r--app/assets/javascripts/organizations/index/graphql/organizations.query.graphql14
-rw-r--r--app/assets/javascripts/organizations/index/index.js33
-rw-r--r--app/assets/javascripts/organizations/mock_data.js40
-rw-r--r--app/assets/javascripts/organizations/new/components/app.vue82
-rw-r--r--app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/new/graphql/typedefs.graphql5
-rw-r--r--app/assets/javascripts/organizations/new/index.js35
-rw-r--r--app/assets/javascripts/organizations/shared/components/new_edit_form.vue125
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/resolvers.js36
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue5
-rw-r--r--app/assets/javascripts/pages/groups/custom_emoji/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/dashboards/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/datasources/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/explore/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/manage/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/work_items/show/index.js4
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue50
-rw-r--r--app/assets/javascripts/pages/organizations/organizations/index/index.js3
-rw-r--r--app/assets/javascripts/pages/organizations/organizations/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js15
-rw-r--r--app/assets/javascripts/pages/projects/find_file/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js373
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/jobs/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js1
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js16
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js4
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue32
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/tree/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/work_items/index.js2
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js4
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue2
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue5
-rw-r--r--app/assets/javascripts/pages/users/terms/index/index.js3
-rw-r--r--app/assets/javascripts/performance_bar/components/add_request.vue10
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue30
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue194
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue11
-rw-r--r--app/assets/javascripts/performance_bar/components/request_warning.vue21
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue17
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_button.vue2
-rw-r--r--app/assets/javascripts/projects/project_find_file.js11
-rw-r--r--app/assets/javascripts/projects/settings/api/access_dropdown_api.js16
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue4
-rw-r--r--app/assets/javascripts/projects/settings/components/transfer_project_form.vue2
-rw-r--r--app/assets/javascripts/ref/components/ambiguous_ref_modal.vue80
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue16
-rw-r--r--app/assets/javascripts/ref/constants.js3
-rw-r--r--app/assets/javascripts/ref/init_ambiguous_ref_modal.js20
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue2
-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.vue25
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_list.vue4
-rw-r--r--app/assets/javascripts/releases/components/releases_empty_state.vue3
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue31
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js143
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/getters.js4
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js1
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutations.js3
-rw-r--r--app/assets/javascripts/repository/components/blob_controls.vue6
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue3
-rw-r--r--app/assets/javascripts/repository/components/commit_info.vue116
-rw-r--r--app/assets/javascripts/repository/components/fork_info.vue2
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue150
-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.vue2
-rw-r--r--app/assets/javascripts/repository/index.js15
-rw-r--r--app/assets/javascripts/repository/queries/blob_controls.query.graphql4
-rw-r--r--app/assets/javascripts/repository/router.js3
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue24
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/data.js9
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/index.vue17
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue5
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/index.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue7
-rw-r--r--app/assets/javascripts/search/sidebar/components/milestones_filters.vue18
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js1
-rw-r--r--app/assets/javascripts/search/store/constants.js4
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue26
-rw-r--r--app/assets/javascripts/search/topbar/constants.js3
-rw-r--r--app/assets/javascripts/search/topbar/index.js11
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue7
-rw-r--r--app/assets/javascripts/sentry/init_sentry.js23
-rw-r--r--app/assets/javascripts/sentry/sentry_browser_wrapper.js16
-rw-r--r--app/assets/javascripts/settings_panels.js2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue50
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue23
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js17
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue5
-rw-r--r--app/assets/javascripts/snippets/components/embed_dropdown.vue2
-rw-r--r--app/assets/javascripts/snippets/components/show.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue3
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_view.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue7
-rw-r--r--app/assets/javascripts/snippets/components/snippet_title.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_visibility_edit.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/brand_logo.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/counter.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/create_menu.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue3
-rw-r--r--app/assets/javascripts/super_sidebar/components/help_center.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue18
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue26
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue14
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue17
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue62
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue9
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue83
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_name_group.vue91
-rw-r--r--app/assets/javascripts/super_sidebar/constants.js2
-rw-r--r--app/assets/javascripts/super_sidebar/event_hub.js3
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js4
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js22
-rw-r--r--app/assets/javascripts/terms/components/app.vue2
-rw-r--r--app/assets/javascripts/terraform/components/empty_state.vue5
-rw-r--r--app/assets/javascripts/token_access/components/inbound_token_access.vue2
-rw-r--r--app/assets/javascripts/tracking/constants.js1
-rw-r--r--app/assets/javascripts/tracking/internal_events.js35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue77
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue44
-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/merge_checks.stories.js91
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue129
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue72
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue53
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/badges/beta_badge.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/badges/experiment_badge.stories.js24
-rw-r--r--app/assets/javascripts/vue_shared/components/badges/experiment_badge.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/badges/hover_badge.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/incubation/pagination.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue99
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/utils.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/utils.js37
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_labels.vue62
-rw-r--r--app/assets/javascripts/vue_shared/components/vuex_module_provider.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue2
-rw-r--r--app/assets/javascripts/vue_shared/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/global_search/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue5
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue32
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue43
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue32
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue5
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue16
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue1
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue9
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue27
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue5
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue5
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue20
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue1
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue26
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue89
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue35
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue15
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue34
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue29
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue17
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue15
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_parent.vue249
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue249
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue15
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue142
-rw-r--r--app/assets/javascripts/work_items/components/work_item_todos.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue5
-rw-r--r--app/assets/javascripts/work_items/constants.js28
-rw-r--r--app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql10
-rw-r--r--app/assets/javascripts/work_items/graphql/cache_utils.js15
-rw-r--r--app/assets/javascripts/work_items/graphql/group_work_item_by_iid.query.graphql12
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/remove_linked_items.mutation.graphql6
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql15
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql8
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql2
-rw-r--r--app/assets/javascripts/work_items/index.js19
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue5
-rw-r--r--app/assets/javascripts/work_items/utils.js5
607 files changed, 8500 insertions, 4811 deletions
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
index 560d733c10c..e005e183c9f 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
@@ -14,8 +14,10 @@ import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import {
ACTIONS_I18N,
NO_ACTION,
+ TRUST_ACTION,
USER_ACTION_OPTIONS,
REASON_OPTIONS,
+ TRUST_REASON,
STATUS_OPEN,
SUCCESS_ALERT,
FAILED_ALERT,
@@ -77,6 +79,16 @@ export default {
userActionOptions() {
return this.isNotCurrentUser ? USER_ACTION_OPTIONS : [NO_ACTION];
},
+ reasonOptions() {
+ if (!this.isNotCurrentUser) {
+ return [];
+ }
+
+ if (this.form.user_action === TRUST_ACTION.value) {
+ return [TRUST_REASON];
+ }
+ return REASON_OPTIONS;
+ },
},
methods: {
toggleActionsDrawer() {
@@ -120,7 +132,6 @@ export default {
},
},
i18n: ACTIONS_I18N,
- reasonOptions: REASON_OPTIONS,
DRAWER_Z_INDEX,
};
</script>
@@ -173,7 +184,7 @@ export default {
id="reason"
v-model="form.reason"
data-testid="reason-select"
- :options="$options.reasonOptions"
+ :options="reasonOptions"
:state="validationState.reason"
@change="validateReason"
/>
diff --git a/app/assets/javascripts/admin/abuse_report/components/user_details.vue b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
index fe0add1ba8d..0c32341652b 100644
--- a/app/assets/javascripts/admin/abuse_report/components/user_details.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
@@ -60,9 +60,6 @@ export default {
data-testid="credit-card-verification"
:label="$options.i18n.creditCard"
>
- <gl-sprintf :message="$options.i18n.registeredWith">
- <template #name>{{ user.creditCard.name }}</template>
- </gl-sprintf>
<gl-sprintf v-if="showSimilarRecords" :message="$options.i18n.similarRecords">
<template #cardMatchesLink="{ content }">
<gl-link :href="user.creditCard.cardMatchesLink">
diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js
index 6cae6b24f20..f028408bed7 100644
--- a/app/assets/javascripts/admin/abuse_report/constants.js
+++ b/app/assets/javascripts/admin/abuse_report/constants.js
@@ -25,11 +25,14 @@ export const ACTIONS_I18N = {
};
export const NO_ACTION = { value: '', text: s__('AbuseReport|No action') };
+export const TRUST_REASON = { value: 'trusted', text: s__(`AbuseReport|Confirmed trusted user`) };
+export const TRUST_ACTION = { value: 'trust_user', text: s__('AbuseReport|Trust user') };
export const USER_ACTION_OPTIONS = [
NO_ACTION,
{ value: 'block_user', text: s__('AbuseReport|Block user') },
{ value: 'ban_user', text: s__('AbuseReport|Ban user') },
+ TRUST_ACTION,
{ value: 'delete_user', text: s__('AbuseReport|Delete user') },
];
@@ -75,7 +78,6 @@ export const USER_DETAILS_I18N = {
reportedFor: s__(
'AbuseReport|%{reportLinkStart}Reported%{reportLinkEnd} for %{category} %{timeAgo}.',
),
- registeredWith: s__('AbuseReport|Registered with name %{name}.'),
similarRecords: s__(
'AbuseReport|Card matches %{cardMatchesLinkStart}%{count} accounts%{cardMatchesLinkEnd}',
),
@@ -87,6 +89,7 @@ export const REPORTED_CONTENT_I18N = {
comment: s__('AbuseReport|Reported comment'),
issue: s__('AbuseReport|Reported issue'),
merge_request: s__('AbuseReport|Reported merge request'),
+ epic: s__('AbuseReport|Reported epic'),
unknown: s__('AbuseReport|Reported content'),
},
viewScreenshot: s__('AbuseReport|View screenshot'),
@@ -96,6 +99,7 @@ export const REPORTED_CONTENT_I18N = {
comment: s__('AbuseReport|Go to comment'),
issue: s__('AbuseReport|Go to issue'),
merge_request: s__('AbuseReport|Go to merge request'),
+ epic: s__('AbuseReport|Go to epic'),
unknown: s__('AbuseReport|Go to content'),
},
reportedBy: s__('AbuseReport|Reported by'),
diff --git a/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue b/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue
index ef4a5319eec..0b640a34864 100644
--- a/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue
+++ b/app/assets/javascripts/admin/application_settings/inactive_project_deletion/components/form.vue
@@ -143,7 +143,7 @@ export default {
v-model="minSizeMb"
:state="isMinSizeMbValid"
name="application_setting[inactive_projects_min_size_mb]"
- size="md"
+ width="md"
type="number"
:min="0"
data-testid="min-size-input"
@@ -177,7 +177,7 @@ export default {
v-model="deleteAfterMonths"
:state="isDeleteAfterMonthsValid"
name="application_setting[inactive_projects_delete_after_months]"
- size="sm"
+ width="sm"
type="number"
:min="0"
data-testid="delete-after-months-input"
@@ -215,7 +215,7 @@ export default {
v-model="sendWarningEmailAfterMonths"
:state="isSendWarningEmailAfterMonthsValid"
name="application_setting[inactive_projects_send_warning_email_after_months]"
- size="sm"
+ width="sm"
type="number"
:min="0"
data-testid="send-warning-email-after-months-input"
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
index 07814ef2511..253eefc323c 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
@@ -42,6 +42,6 @@ export default {
<template>
<div class="gl-display-flex gl-gap-3 gl-align-items-center">
<gl-datepicker v-model="date" />
- <gl-form-input v-model="time" size="sm" type="time" data-testid="time-picker" />
+ <gl-form-input v-model="time" width="sm" type="time" data-testid="time-picker" />
</div>
</template>
diff --git a/app/assets/javascripts/alert.js b/app/assets/javascripts/alert.js
index 006c4f50d09..4d724b17723 100644
--- a/app/assets/javascripts/alert.js
+++ b/app/assets/javascripts/alert.js
@@ -1,6 +1,7 @@
import * as Sentry from '@sentry/browser';
import Vue from 'vue';
-import { GlAlert } from '@gitlab/ui';
+import isEmpty from 'lodash/isEmpty';
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
export const VARIANT_SUCCESS = 'success';
@@ -32,6 +33,14 @@ export const VARIANT_TIP = 'tip';
* // Respond to the alert being dismissed
* createAlert({ message: 'Message', onDismiss: () => {} });
*
+ * @example
+ * // Add inline link in the message
+ * createAlert({ message: 'Read more at %{exampleLinkStart}example page%{exampleLinkEnd}.', messageLinks: { exampleLink: 'https://example.com' } });
+ *
+ * @example
+ * // Add inline links in the message with custom GlLink props
+ * createAlert({ message: 'Read more at %{exampleLinkStart}example page%{exampleLinkEnd}.', messageLinks: { exampleLink: { href: 'https://example.com', target: '_blank', isUnsafeLink: true }} });
+ *
* @param {object} options - Options to control the flash message
* @param {string} options.message - Alert message text
* @param {string} [options.title] - Alert title
@@ -48,6 +57,7 @@ export const VARIANT_TIP = 'tip';
* @param {string} [options.secondaryButton.link] - Href of secondary button
* @param {string} [options.secondaryButton.text] - Text of secondary button
* @param {Function} [options.secondaryButton.clickHandler] - Handler to call when secondary button is clicked on. The click event is sent as an argument.
+ * @param {object} [options.messageLinks] - Object containing mapping of sprintf tokens to URLs, used to format links within the message. If needed, you can pass a full props object for GlLink instead of a URL string
* @param {boolean} [options.captureError] - Whether to send error to Sentry
* @param {object} [options.error] - Error to be captured in Sentry
*/
@@ -63,6 +73,7 @@ export const createAlert = ({
onDismiss = null,
captureError = false,
error = null,
+ messageLinks = null,
}) => {
if (captureError && error) Sentry.captureException(error);
@@ -76,6 +87,45 @@ export const createAlert = ({
alertContainer.replaceChildren(el);
}
+ const createMessageNodes = (h) => {
+ if (isEmpty(messageLinks)) {
+ return message;
+ }
+
+ const normalizeLinkProps = (hrefOrProps) => {
+ const { href, ...otherLinkProps } =
+ typeof hrefOrProps === 'string' ? { href: hrefOrProps } : hrefOrProps;
+
+ return { href, linkProps: otherLinkProps };
+ };
+
+ return [
+ h(GlSprintf, {
+ props: {
+ message,
+ },
+ scopedSlots: Object.assign(
+ {},
+ ...Object.entries(messageLinks).map(([slotName, hrefOrProps]) => {
+ const { href, linkProps } = normalizeLinkProps(hrefOrProps);
+
+ return {
+ [slotName]: (props) =>
+ h(
+ GlLink,
+ {
+ props: linkProps,
+ attrs: { href },
+ },
+ props.content,
+ ),
+ };
+ }),
+ ),
+ }),
+ ];
+ };
+
return new Vue({
el,
components: {
@@ -130,7 +180,7 @@ export const createAlert = ({
},
on,
},
- message,
+ createMessageNodes(h),
);
},
});
diff --git a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
index 77c14d9f812..da9f300c941 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
@@ -34,7 +34,11 @@ export default {
</script>
<template>
<div>
- <gl-empty-state :title="$options.i18n.emptyState.title" :svg-path="emptyAlertSvgPath">
+ <gl-empty-state
+ :title="$options.i18n.emptyState.title"
+ :svg-path="emptyAlertSvgPath"
+ :svg-height="null"
+ >
<template #description>
<div class="gl-display-block">
<span>{{ $options.i18n.emptyState.info }}</span>
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
index 84ee8f41b11..39fbc217278 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
@@ -56,6 +56,7 @@ export default {
'hasNoAccessError',
'groupPath',
'namespace',
+ 'predefinedDateRange',
]),
...mapGetters(['pathNavigationData', 'filterParams']),
isLoaded() {
@@ -132,6 +133,7 @@ export default {
'fetchStageData',
'setSelectedStage',
'setDateRange',
+ 'setPredefinedDateRange',
'updateStageTablePagination',
]),
onSetDateRange({ startDate, endDate }) {
@@ -170,7 +172,9 @@ export default {
:start-date="createdAfter"
:end-date="createdBefore"
:group-path="groupPath"
+ :predefined-date-range="predefinedDateRange"
@setDateRange="onSetDateRange"
+ @setPredefinedDateRange="setPredefinedDateRange"
/>
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row">
<path-navigation
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue
index 38f9936c7c1..898633868cd 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue
@@ -195,6 +195,7 @@ export default {
:title="emptyStateTitleText"
:description="emptyStateMessage"
:svg-path="noDataSvgPath"
+ :svg-height="null"
/>
<gl-table
v-else
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
index 0de62013a63..775c3827fc7 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
@@ -2,7 +2,17 @@
import { GlTooltipDirective } from '@gitlab/ui';
import DateRange from '~/analytics/shared/components/daterange.vue';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
-import { DATE_RANGE_LIMIT, PROJECTS_PER_PAGE } from '~/analytics/shared/constants';
+import {
+ DATE_RANGE_LIMIT,
+ DATE_RANGE_CUSTOM_VALUE,
+ PROJECTS_PER_PAGE,
+ MAX_DATE_RANGE_TEXT,
+ DATE_RANGE_LAST_30_DAYS_VALUE,
+ LAST_30_DAYS,
+} from '~/analytics/shared/constants';
+import { getCurrentUtcDate, datesMatch } from '~/lib/utils/datetime_utility';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import DateRangesDropdown from '~/analytics/shared/components/date_ranges_dropdown.vue';
import FilterBar from './filter_bar.vue';
export default {
@@ -11,10 +21,12 @@ export default {
DateRange,
ProjectsDropdownFilter,
FilterBar,
+ DateRangesDropdown,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
selectedProjects: {
type: Array,
@@ -31,6 +43,11 @@ export default {
required: false,
default: true,
},
+ hasPredefinedDateRangesFilter: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
namespacePath: {
type: String,
required: true,
@@ -49,6 +66,11 @@ export default {
required: false,
default: null,
},
+ predefinedDateRange: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
projectsQueryParams() {
@@ -58,42 +80,104 @@ export default {
};
},
currentDate() {
- const now = new Date();
- return new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
+ return getCurrentUtcDate();
+ },
+ isDefaultDateRange() {
+ return datesMatch(this.startDate, LAST_30_DAYS) && datesMatch(this.endDate, this.currentDate);
+ },
+ supportsPredefinedDateRanges() {
+ return this.glFeatures?.vsaPredefinedDateRanges;
+ },
+ dateRangeOption() {
+ const { predefinedDateRange } = this;
+
+ if (predefinedDateRange) return predefinedDateRange;
+
+ if (!predefinedDateRange && !this.isDefaultDateRange) return DATE_RANGE_CUSTOM_VALUE;
+
+ return DATE_RANGE_LAST_30_DAYS_VALUE;
+ },
+ isCustomDateRangeSelected() {
+ return this.dateRangeOption === DATE_RANGE_CUSTOM_VALUE;
+ },
+ shouldShowPredefinedDateRanges() {
+ return this.supportsPredefinedDateRanges && this.hasPredefinedDateRangesFilter;
+ },
+ shouldShowDateRangePicker() {
+ if (this.shouldShowPredefinedDateRanges) {
+ return this.hasDateRangeFilter && this.isCustomDateRangeSelected;
+ }
+
+ return this.hasDateRangeFilter;
+ },
+ maxDateRangeTooltip() {
+ return this.$options.i18n.maxDateRangeTooltip(this.$options.maxDateRange);
+ },
+ shouldShowDateRangeFilters() {
+ return this.hasDateRangeFilter || this.hasPredefinedDateRangesFilter;
+ },
+ shouldShowFilterDropdowns() {
+ return this.hasProjectFilter || this.shouldShowDateRangeFilters;
+ },
+ },
+ methods: {
+ onSelectPredefinedDateRange({ value, startDate, endDate }) {
+ this.$emit('setPredefinedDateRange', value);
+ this.$emit('setDateRange', { startDate, endDate });
+ },
+ onSelectCustomDateRange() {
+ this.$emit('setPredefinedDateRange', DATE_RANGE_CUSTOM_VALUE);
},
},
multiProjectSelect: true,
maxDateRange: DATE_RANGE_LIMIT,
+ i18n: {
+ maxDateRangeTooltip: MAX_DATE_RANGE_TEXT,
+ },
};
</script>
<template>
<div
- class="gl-mt-3 gl-py-2 gl-px-3 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-t-1 gl-border-t-solid gl-border-gray-100"
+ class="gl-mt-3 gl-py-5 gl-px-3 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-t-1 gl-border-t-solid gl-border-gray-100"
>
<filter-bar
data-testid="vsa-filter-bar"
- class="filtered-search-box gl-display-flex gl-mb-2 gl-mr-3 gl-border-none"
+ class="filtered-search-box gl-display-flex gl-border-none"
:namespace-path="namespacePath"
/>
+ <hr v-if="shouldShowFilterDropdowns" class="gl-my-5" />
<div
- v-if="hasDateRangeFilter || hasProjectFilter"
- class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between"
+ v-if="shouldShowFilterDropdowns"
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-gap-5"
>
- <div>
- <projects-dropdown-filter
- v-if="hasProjectFilter"
- toggle-classes="gl-max-w-26"
- class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0"
- :group-namespace="groupPath"
- :query-params="projectsQueryParams"
- :multi-select="$options.multiProjectSelect"
- :default-projects="selectedProjects"
- @selected="$emit('selectProject', $event)"
+ <projects-dropdown-filter
+ v-if="hasProjectFilter"
+ toggle-classes="gl-max-w-26"
+ class="js-projects-dropdown-filter project-select"
+ :group-namespace="groupPath"
+ :query-params="projectsQueryParams"
+ :multi-select="$options.multiProjectSelect"
+ :default-projects="selectedProjects"
+ @selected="$emit('selectProject', $event)"
+ />
+ <div
+ v-if="shouldShowDateRangeFilters"
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-gap-3"
+ data-testid="vsa-date-range-filter-container"
+ >
+ <date-ranges-dropdown
+ v-if="shouldShowPredefinedDateRanges"
+ data-testid="vsa-predefined-date-ranges-dropdown"
+ :selected="dateRangeOption"
+ :tooltip="maxDateRangeTooltip"
+ include-end-date-in-days-selected
+ :include-custom-date-range-option="hasDateRangeFilter"
+ @selected="onSelectPredefinedDateRange"
+ @customDateRangeSelected="onSelectCustomDateRange"
/>
- </div>
- <div class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row">
<date-range
- v-if="hasDateRangeFilter"
+ v-if="shouldShowDateRangePicker"
+ data-testid="vsa-date-range-picker"
:start-date="startDate"
:end-date="endDate"
:max-date="currentDate"
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/actions.js b/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
index 32fe0abe83e..90ac531aa87 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
@@ -163,6 +163,10 @@ export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore
return dispatch('refetchStageData');
};
+export const setPredefinedDateRange = ({ commit }, predefinedDateRange) => {
+ commit(types.SET_PREDEFINED_DATE_RANGE, predefinedDateRange);
+};
+
export const setInitialStage = ({ dispatch, commit, state: { stages } }, stage) => {
if (!stages.length && !stage) {
commit(types.SET_NO_ACCESS_ERROR);
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js
index 9376d81f317..e0a7a4292e2 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js
@@ -4,6 +4,7 @@ export const SET_LOADING = 'SET_LOADING';
export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const SET_DATE_RANGE = 'SET_DATE_RANGE';
+export const SET_PREDEFINED_DATE_RANGE = 'SET_PREDEFINED_DATE_RANGE';
export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_NO_ACCESS_ERROR = 'SET_NO_ACCESS_ERROR';
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
index 4af96fc96e3..4fa88279fe0 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
@@ -34,6 +34,9 @@ export default {
state.createdBefore = createdBefore;
state.createdAfter = createdAfter;
},
+ [types.SET_PREDEFINED_DATE_RANGE](state, predefinedDateRange) {
+ state.predefinedDateRange = predefinedDateRange;
+ },
[types.SET_PAGINATION](state, { page, hasNextPage, sort, direction }) {
Vue.set(state, 'pagination', {
page,
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/state.js b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
index 0c51656c59f..3d9b56b043d 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
@@ -32,4 +32,5 @@ export default () => ({
sort: PAGINATION_SORT_FIELD_END_EVENT,
direction: PAGINATION_SORT_DIRECTION_DESC,
},
+ predefinedDateRange: null,
});
diff --git a/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
index 593de1dcee7..91f0019913c 100644
--- a/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
@@ -72,6 +72,7 @@ export default {
v-if="isEmpty"
:title="__('Data is still calculating...')"
:svg-path="noDataImagePath"
+ :svg-height="null"
>
<template #description>
<p class="gl-mb-0">{{ __('It may be several days before you see feature usage data.') }}</p>
diff --git a/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue b/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
index b9501107e37..c2e1e3f1bad 100644
--- a/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
+++ b/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
@@ -24,7 +24,11 @@ export default {
};
</script>
<template>
- <gl-empty-state :title="s__('ServicePing|Service ping is off')" :svg-path="svgPath">
+ <gl-empty-state
+ :title="s__('ServicePing|Service ping is off')"
+ :svg-path="svgPath"
+ :svg-height="null"
+ >
<template #description>
<gl-sprintf
v-if="!isAdmin"
diff --git a/app/assets/javascripts/analytics/shared/components/date_ranges_dropdown.vue b/app/assets/javascripts/analytics/shared/components/date_ranges_dropdown.vue
new file mode 100644
index 00000000000..7ea7aba6f44
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/components/date_ranges_dropdown.vue
@@ -0,0 +1,131 @@
+<script>
+import { GlCollapsibleListbox, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+
+import { isString } from 'lodash';
+import { isValidDate, getDayDifference } from '~/lib/utils/datetime_utility';
+import {
+ DATE_RANGE_CUSTOM_VALUE,
+ DEFAULT_DATE_RANGE_OPTIONS,
+ NUMBER_OF_DAYS_SELECTED,
+} from '~/analytics/shared/constants';
+import { __ } from '~/locale';
+
+export default {
+ name: 'DateRangesDropdown',
+ components: {
+ GlCollapsibleListbox,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ dateRangeOptions: {
+ type: Array,
+ required: false,
+ default: () => DEFAULT_DATE_RANGE_OPTIONS,
+ validator: (options) =>
+ options.length &&
+ options.every(
+ ({ text, value, startDate, endDate }) =>
+ isString(text) &&
+ isString(value) &&
+ isValidDate(startDate) &&
+ isValidDate(endDate) &&
+ endDate >= startDate,
+ ),
+ },
+ selected: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tooltip: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ includeCustomDateRangeOption: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ includeEndDateInDaysSelected: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ selectedValue: this.selected || this.dateRangeOptions[0].value,
+ };
+ },
+ computed: {
+ items() {
+ const dateRangeOptions = this.dateRangeOptions.map(({ text, value }) => ({ text, value }));
+
+ if (!this.includeCustomDateRangeOption) return dateRangeOptions;
+
+ return [...dateRangeOptions, this.$options.customDateRangeItem];
+ },
+ isCustomDateRangeSelected() {
+ return this.selectedValue === DATE_RANGE_CUSTOM_VALUE;
+ },
+ groupedDateRangeOptionsByValue() {
+ return this.dateRangeOptions.reduce((acc, { value, startDate, endDate }) => {
+ acc[value] = { startDate, endDate };
+
+ return acc;
+ }, {});
+ },
+ selectedDateRange() {
+ if (this.isCustomDateRangeSelected) return null;
+
+ return this.groupedDateRangeOptionsByValue[this.selectedValue];
+ },
+ showDaysSelectedCount() {
+ return !this.isCustomDateRangeSelected && this.daysSelectedCount;
+ },
+ daysSelectedCount() {
+ const { selectedDateRange } = this;
+
+ if (!selectedDateRange) return '';
+
+ const { startDate, endDate } = selectedDateRange;
+
+ const daysCount = getDayDifference(startDate, endDate);
+
+ return this.$options.i18n.daysSelected(
+ this.includeEndDateInDaysSelected ? daysCount + 1 : daysCount,
+ );
+ },
+ },
+ methods: {
+ onSelect(value) {
+ if (this.isCustomDateRangeSelected) {
+ this.$emit('customDateRangeSelected');
+ } else {
+ this.$emit('selected', { value, ...this.selectedDateRange });
+ }
+ },
+ },
+ customDateRangeItem: {
+ text: __('Custom'),
+ value: DATE_RANGE_CUSTOM_VALUE,
+ },
+ i18n: {
+ daysSelected: NUMBER_OF_DAYS_SELECTED,
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center gl-gap-3">
+ <gl-collapsible-listbox v-model="selectedValue" :items="items" @select="onSelect" />
+ <div v-if="showDaysSelectedCount" class="gl-text-gray-500">
+ <span data-testid="predefined-date-range-days-count">{{ daysSelectedCount }}</span>
+ <gl-icon v-if="tooltip" v-gl-tooltip class="gl-ml-2" name="information-o" :title="tooltip" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/shared/components/daterange.vue b/app/assets/javascripts/analytics/shared/components/daterange.vue
index f47e0ccbbf2..2126359cfe4 100644
--- a/app/assets/javascripts/analytics/shared/components/daterange.vue
+++ b/app/assets/javascripts/analytics/shared/components/daterange.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlDaterangePicker } from '@gitlab/ui';
-import { n__, __, sprintf } from '~/locale';
+import { MAX_DATE_RANGE_TEXT, NUMBER_OF_DAYS_SELECTED } from '~/analytics/shared/constants';
export default {
components: {
@@ -46,18 +46,6 @@ export default {
default: false,
},
},
- data() {
- return {
- maxDateRangeTooltip: sprintf(
- __(
- 'Showing data for workflow items completed in this date range. Date range limited to %{maxDateRange} days.',
- ),
- {
- maxDateRange: this.maxDateRange,
- },
- ),
- };
- },
computed: {
dateRange: {
get() {
@@ -67,12 +55,19 @@ export default {
this.$emit('change', { startDate, endDate });
},
},
+ maxDateRangeTooltip() {
+ return this.$options.i18n.maxDateRangeTooltip(this.maxDateRange);
+ },
},
methods: {
numberOfDays(daysSelected) {
- return n__('1 day selected', '%d days selected', daysSelected);
+ return this.$options.i18n.daysSelected(daysSelected);
},
},
+ i18n: {
+ maxDateRangeTooltip: MAX_DATE_RANGE_TEXT,
+ daysSelected: NUMBER_OF_DAYS_SELECTED,
+ },
};
</script>
<template>
diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
index ddfc6baafa9..662451c5eb4 100644
--- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlIcon, GlAvatar, GlCollapsibleListbox, GlTruncate } from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, unionBy } from 'lodash';
import { filterBySearchTerm } from '~/analytics/shared/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
@@ -8,6 +8,8 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { n__, s__, __ } from '~/locale';
import getProjects from '../graphql/projects.query.graphql';
+const MIN_SEARCH_CHARS = 3;
+
const sortByProjectName = (projects = []) => projects.sort((a, b) => a.name.localeCompare(b.name));
const mapItemToListboxFormat = (item) => ({ ...item, value: item.id, text: item.name });
@@ -98,10 +100,6 @@ export default {
availableProjects() {
return filterBySearchTerm(this.projects, this.searchTerm);
},
- noResultsAvailable() {
- const { loading, availableProjects } = this;
- return !loading && !availableProjects.length;
- },
selectedItems() {
return sortByProjectName(this.selectedProjects);
},
@@ -152,6 +150,9 @@ export default {
singleSelectedProject(selectedObj, isMarking) {
return isMarking ? [selectedObj] : [];
},
+ getSelectedProjects(projects, selectedProjectIds) {
+ return projects.filter(({ id }) => selectedProjectIds.includes(id));
+ },
setSelectedProjects(payload) {
this.selectedProjects = this.multiSelect
? payload
@@ -163,8 +164,10 @@ export default {
this.handleUpdatedSelectedProjects();
},
onMultiSelectClick(projectIds) {
- const projects = this.availableProjects.filter(({ id }) => projectIds.includes(id));
- this.setSelectedProjects(projects);
+ const newlySelectedProjects = this.getSelectedProjects(this.availableProjects, projectIds);
+ const selectedProjects = this.getSelectedProjects(this.selectedProjects, projectIds);
+
+ this.setSelectedProjects(unionBy(newlySelectedProjects, selectedProjects, 'id'));
this.isDirty = true;
},
onSelected(payload) {
@@ -219,7 +222,12 @@ export default {
return getIdFromGraphQLId(project.id);
},
setSearchTerm(val) {
- this.searchTerm = val;
+ if (val && val.length >= MIN_SEARCH_CHARS) {
+ this.searchTerm = val;
+ return;
+ }
+
+ this.searchTerm = '';
},
},
AVATAR_SHAPE_OPTION_RECT,
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index 7ec7eac24ec..f0d9bf201e5 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -1,9 +1,17 @@
import dateFormat, { masks } from '~/lib/dateformat';
-import { nDaysBefore, getStartOfDay } from '~/lib/utils/datetime_utility';
-import { s__ } from '~/locale';
+import {
+ nDaysBefore,
+ getStartOfDay,
+ dayAfter,
+ getDateInPast,
+ getCurrentUtcDate,
+ nWeeksBefore,
+} from '~/lib/utils/datetime_utility';
+import { s__, __, sprintf, n__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export const DATE_RANGE_LIMIT = 180;
+export const DEFAULT_DATE_RANGE = 29; // 30 including current date
export const PROJECTS_PER_PAGE = 50;
const { isoDate, mediumDate } = masks;
@@ -14,10 +22,63 @@ export const dateFormats = {
month: 'mmmm',
};
+const TODAY = getCurrentUtcDate();
+const TOMORROW = dayAfter(TODAY, { utc: true });
+export const LAST_30_DAYS = getDateInPast(TOMORROW, 30, { utc: true });
+
const startOfToday = getStartOfDay(new Date(), { utc: true });
-const last180Days = nDaysBefore(startOfToday, DATE_RANGE_LIMIT, { utc: true });
+const lastXDays = __('Last %{days} days');
+const lastWeek = nWeeksBefore(TOMORROW, 1, { utc: true });
+const last90Days = getDateInPast(TOMORROW, 90, { utc: true });
+const last180Days = getDateInPast(TOMORROW, DATE_RANGE_LIMIT, { utc: true });
+const mrThroughputStartDate = nDaysBefore(startOfToday, DATE_RANGE_LIMIT, { utc: true });
const formatDateParam = (d) => dateFormat(d, dateFormats.isoDate, true);
+export const DATE_RANGE_CUSTOM_VALUE = 'custom';
+export const DATE_RANGE_LAST_30_DAYS_VALUE = 'last_30_days';
+
+export const DEFAULT_DATE_RANGE_OPTIONS = [
+ {
+ text: __('Last week'),
+ value: 'last_week',
+ startDate: lastWeek,
+ endDate: TODAY,
+ },
+ {
+ text: sprintf(lastXDays, { days: 30 }),
+ value: DATE_RANGE_LAST_30_DAYS_VALUE,
+ startDate: LAST_30_DAYS,
+ endDate: TODAY,
+ },
+ {
+ text: sprintf(lastXDays, { days: 90 }),
+ value: 'last_90_days',
+ startDate: last90Days,
+ endDate: TODAY,
+ },
+ {
+ text: sprintf(lastXDays, { days: 180 }),
+ value: 'last_180_days',
+ startDate: last180Days,
+ endDate: TODAY,
+ },
+];
+
+export const MAX_DATE_RANGE_TEXT = (maxDateRange) => {
+ return sprintf(
+ __(
+ 'Showing data for workflow items completed in this date range. Date range limited to %{maxDateRange} days.',
+ ),
+ {
+ maxDateRange,
+ },
+ );
+};
+
+export const NUMBER_OF_DAYS_SELECTED = (numDays) => {
+ return n__('1 day selected', '%d days selected', numDays);
+};
+
export const METRIC_POPOVER_LABEL = s__('ValueStreamAnalytics|View details');
export const ISSUES_COMPLETED_TYPE = 'issues_completed';
@@ -147,7 +208,7 @@ export const METRIC_TOOLTIPS = {
description: s__('ValueStreamAnalytics|The number of merge requests merged by month.'),
groupLink: '-/analytics/productivity_analytics',
projectLink: `-/analytics/merge_request_analytics?start_date=${formatDateParam(
- last180Days,
+ mrThroughputStartDate,
)}&end_date=${formatDateParam(startOfToday)}`,
docsLink: helpPagePath('user/analytics/merge_request_analytics', {
anchor: 'view-the-number-of-merge-requests-in-a-date-range',
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 6dfc1c609de..185cdaa1c99 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -33,7 +33,6 @@ const Api = {
forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename',
- projectGroupsPath: '/api/:version/projects/:id/groups.json',
projectUsersPath: '/api/:version/projects/:id/users',
projectInvitationsPath: '/api/:version/projects/:id/invitations',
projectMembersPath: '/api/:version/projects/:id/members',
@@ -178,19 +177,6 @@ const Api = {
});
},
- projectGroups(id, options) {
- const url = Api.buildUrl(this.projectGroupsPath).replace(':id', encodeURIComponent(id));
-
- return axios
- .get(url, {
- params: {
- ...options,
- },
- })
- .then(({ data }) => {
- return data;
- });
- },
/**
* @deprecated This method will be removed soon. Use the
* `getGroups` method in `~/rest_api` instead.
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 31531c90b94..1cd5854740e 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -80,7 +80,7 @@ export default {
:href="linkUrl"
target="_blank"
rel="noopener noreferrer"
- data-qa-selector="badge_image_link"
+ data-testid="badge-image-link"
:data-qa-link-url="linkUrl"
>
<img
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index b69890572eb..12c9662b30d 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -100,7 +100,7 @@ export default {
<template>
<div>
<gl-loading-icon v-show="isLoading" size="md" />
- <div data-qa-selector="badge_list_content">
+ <div data-testid="badge-list-content">
<gl-table
:empty-text="emptyMessage"
:fields="fields"
@@ -109,7 +109,7 @@ export default {
:current-page="currentPage"
stacked="md"
show-empty
- data-qa-selector="badge_list"
+ data-testid="badge-list"
>
<template #cell(name)="{ item }">
<label class="label-bold str-truncated mb-0">{{ item.name }}</label>
diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
index 42fc85cc5fb..2745ccb4682 100644
--- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
@@ -31,14 +31,14 @@ export default {
},
},
methods: {
- ...mapActions('diffs', ['setCurrentFileHash']),
+ ...mapActions('diffs', ['goToFile']),
...mapActions('batchComments', ['scrollToDraft']),
isOnLatestDiff(draft) {
return draft.position?.head_sha === this.getNoteableData.diff_head_sha;
},
async onClickDraft(draft) {
- if (this.viewDiffsFileByFile && draft.file_hash) {
- await this.setCurrentFileHash(draft.file_hash);
+ if (this.viewDiffsFileByFile) {
+ await this.goToFile({ path: draft.file_path });
}
if (draft.position && !this.isOnLatestDiff(draft)) {
@@ -54,7 +54,7 @@ export default {
</script>
<template>
- <gl-disclosure-dropdown :items="listItems" dropup data-qa-selector="review_preview_dropdown">
+ <gl-disclosure-dropdown :items="listItems" dropup data-testid="review-preview-dropdown">
<template #toggle>
<gl-button>
{{ __('Pending comments') }}
diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue
index 00bb9250403..365b7930dd3 100644
--- a/app/assets/javascripts/batch_comments/components/review_bar.vue
+++ b/app/assets/javascripts/batch_comments/components/review_bar.vue
@@ -40,7 +40,7 @@ export default {
<nav class="review-bar-component js-review-bar" data-testid="review_bar_component">
<div
class="review-bar-content d-flex gl-justify-content-end"
- data-qa-selector="review_bar_content"
+ data-testid="review-bar-content"
>
<preview-dropdown />
<submit-dropdown />
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
index 72116b1eb7f..fac45f32464 100644
--- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
@@ -6,7 +6,6 @@ import { __ } from '~/locale';
import { createAlert } from '~/alert';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
@@ -23,7 +22,6 @@ export default {
SummarizeMyReview: () =>
import('ee_component/batch_comments/components/summarize_my_review.vue'),
},
- mixins: [glFeatureFlagsMixin()],
inject: {
canSummarize: { default: false },
},
@@ -127,7 +125,7 @@ export default {
dropup
class="submit-review-dropdown"
:class="{ 'submit-review-dropdown-animated': shouldAnimateReviewButton }"
- data-qa-selector="submit_review_dropdown"
+ data-testid="submit-review-dropdown"
variant="info"
category="primary"
>
@@ -151,7 +149,6 @@ export default {
<markdown-editor
ref="markdownEditor"
v-model="noteData.note"
- :enable-content-editor="Boolean(glFeatures.contentEditorOnIssues)"
class="js-no-autosize"
:is-submitting="isSubmitting"
:render-markdown-path="getNoteableData.preview_note_path"
@@ -192,7 +189,6 @@ export default {
type="submit"
class="js-no-auto-disable"
data-testid="submit-review-button"
- data-qa-selector="submit_review_button"
>
{{ __('Submit review') }}
</gl-button>
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index 181d841a068..6787efbeafa 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,11 +1,6 @@
import Autosize from 'autosize';
-import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
-waitForCSSLoaded(() => {
- const autosizeEls = document.querySelectorAll('.js-autosize');
+const autosizeEls = document.querySelectorAll('.js-autosize');
- Autosize(autosizeEls);
- Autosize.update(autosizeEls);
-
- autosizeEls.forEach((el) => el.classList.add('js-autosize-initialized'));
-});
+Autosize(autosizeEls);
+Autosize.update(autosizeEls);
diff --git a/app/assets/javascripts/behaviors/components/global_alerts.vue b/app/assets/javascripts/behaviors/components/global_alerts.vue
new file mode 100644
index 00000000000..d7333619110
--- /dev/null
+++ b/app/assets/javascripts/behaviors/components/global_alerts.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+
+import { getGlobalAlerts, setGlobalAlerts, removeGlobalAlertById } from '~/lib/utils/global_alerts';
+
+export default {
+ name: 'GlobalAlerts',
+ components: { GlAlert },
+ data() {
+ return {
+ alerts: [],
+ };
+ },
+ mounted() {
+ const { page } = document.body.dataset;
+ const alerts = getGlobalAlerts();
+
+ const alertsToPersist = alerts.filter((alert) => alert.persistOnPages.length);
+ const alertsToRender = alerts.filter(
+ (alert) => alert.persistOnPages.length === 0 || alert.persistOnPages.includes(page),
+ );
+
+ this.alerts = alertsToRender;
+
+ // Once we render the global alerts, we re-set the global alerts to only store persistent alerts for the next load.
+ setGlobalAlerts(alertsToPersist);
+ },
+ methods: {
+ onDismiss(index) {
+ const alert = this.alerts[index];
+ this.alerts.splice(index, 1);
+ removeGlobalAlertById(alert.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="alerts.length">
+ <gl-alert
+ v-for="(alert, index) in alerts"
+ :key="alert.id"
+ :variant="alert.variant"
+ :title="alert.title"
+ :dismissible="alert.dismissible"
+ @dismiss="onDismiss(index)"
+ >{{ alert.message }}</gl-alert
+ >
+ </div>
+</template>
diff --git a/app/assets/javascripts/behaviors/global_alerts.js b/app/assets/javascripts/behaviors/global_alerts.js
new file mode 100644
index 00000000000..476291e6b47
--- /dev/null
+++ b/app/assets/javascripts/behaviors/global_alerts.js
@@ -0,0 +1,17 @@
+import Vue from 'vue';
+
+import GlobalAlerts from './components/global_alerts.vue';
+
+export const initGlobalAlerts = () => {
+ const el = document.getElementById('js-global-alerts');
+
+ if (!el) return false;
+
+ return new Vue({
+ el,
+ name: 'GlobalAlertsRoot',
+ render(createElement) {
+ return createElement(GlobalAlerts);
+ },
+ });
+};
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 871b1279ce6..dc9153e61f7 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -9,6 +9,7 @@ import './quick_submit';
import './requires_input';
import initPageShortcuts from './shortcuts';
import { initToastMessages } from './toasts';
+import { initGlobalAlerts } from './global_alerts';
import './toggler_behavior';
import './preview_markdown';
@@ -24,6 +25,8 @@ initCollapseSidebarOnWindowResize();
initToastMessages();
+initGlobalAlerts();
+
window.requestIdleCallback(
() => {
// Check if we have to Load GFM Input
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 333858f717c..58b08772337 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -3,7 +3,6 @@ import highlightCurrentUser from './highlight_current_user';
import { renderKroki } from './render_kroki';
import renderMath from './render_math';
import renderSandboxedMermaid from './render_sandboxed_mermaid';
-import renderObservability from './render_observability';
import { renderJSONTable } from './render_json_table';
function initPopovers(elements) {
@@ -21,16 +20,7 @@ export function renderGFM(element) {
return;
}
- const [
- highlightEls,
- krokiEls,
- mathEls,
- mermaidEls,
- tableEls,
- userEls,
- popoverEls,
- observabilityEls,
- ] = [
+ const [highlightEls, krokiEls, mathEls, mermaidEls, tableEls, userEls, popoverEls] = [
'.js-syntax-highlight',
'.js-render-kroki[hidden]',
'.js-render-math',
@@ -38,7 +28,6 @@ export function renderGFM(element) {
'[lang="json"][data-lang-params="table"]',
'.gfm-project_member',
'.gfm-issue, .gfm-work_item, .gfm-merge_request, .gfm-epic',
- '.js-render-observability',
].map((selector) => Array.from(element.querySelectorAll(selector)));
syntaxHighlight(highlightEls);
@@ -47,6 +36,5 @@ export function renderGFM(element) {
renderSandboxedMermaid(mermaidEls);
renderJSONTable(tableEls.map((e) => e.parentNode));
highlightCurrentUser(userEls);
- renderObservability(observabilityEls);
initPopovers(popoverEls);
}
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 7525fc76d16..4cba3eccb45 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -22,6 +22,31 @@ const waitForReflow = (fn) => {
window.requestIdleCallback(fn);
};
+const katexOptions = (el) => {
+ const options = {
+ displayMode: el.dataset.mathStyle === 'display',
+ throwOnError: true,
+ trust: (context) =>
+ // this config option restores the KaTeX pre-v0.11.0
+ // behavior of allowing certain commands and protocols
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ ['\\url', '\\href'].includes(context.command) &&
+ ['http', 'https', 'mailto', '_relative'].includes(context.protocol),
+ };
+
+ if (gon.math_rendering_limits_enabled) {
+ options.maxSize = MAX_USER_SPECIFIED_EMS;
+ // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111107 for
+ // reasoning behind this value
+ options.maxExpand = MAX_MACRO_EXPANSIONS;
+ } else {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ options.maxExpand = 'Infinity';
+ }
+
+ return options;
+};
+
/**
* Renders math blocks sequentially while protecting against DoS attacks. Math blocks have a maximum character limit of MAX_MATH_CHARS. If rendering math takes longer than MAX_RENDER_TIME_MS, all subsequent math blocks are skipped and an error message is shown.
*/
@@ -60,7 +85,10 @@ class SafeMathRenderer {
}
const el = chosenEl || this.queue.shift();
- const forceRender = Boolean(chosenEl) || unrestrictedPages.includes(this.pageName);
+ const forceRender =
+ Boolean(chosenEl) ||
+ unrestrictedPages.includes(this.pageName) ||
+ !gon.math_rendering_limits_enabled;
const text = el.textContent;
el.removeAttribute('style');
@@ -128,20 +156,7 @@ class SafeMathRenderer {
}
// eslint-disable-next-line no-unsanitized/property
- displayContainer.innerHTML = this.katex.renderToString(text, {
- displayMode: el.dataset.mathStyle === 'display',
- throwOnError: true,
- maxSize: MAX_USER_SPECIFIED_EMS,
- // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111107 for
- // reasoning behind this value
- maxExpand: MAX_MACRO_EXPANSIONS,
- trust: (context) =>
- // this config option restores the KaTeX pre-v0.11.0
- // behavior of allowing certain commands and protocols
- // eslint-disable-next-line @gitlab/require-i18n-strings
- ['\\url', '\\href'].includes(context.command) &&
- ['http', 'https', 'mailto', '_relative'].includes(context.protocol),
- });
+ displayContainer.innerHTML = this.katex.renderToString(text, katexOptions(el));
} catch (e) {
// Don't show a flash for now because it would override an existing flash message
if (e.message.match(/Too many expansions/)) {
diff --git a/app/assets/javascripts/behaviors/markdown/render_observability.js b/app/assets/javascripts/behaviors/markdown/render_observability.js
deleted file mode 100644
index 6346fb8ab48..00000000000
--- a/app/assets/javascripts/behaviors/markdown/render_observability.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import Vue from 'vue';
-import ObservabilityApp from '~/observability/components/observability_app.vue';
-import { SKELETON_VARIANT_EMBED, INLINE_EMBED_DIMENSIONS } from '~/observability/constants';
-
-const mountVueComponent = (element) => {
- const url = element.dataset.frameUrl;
- return new Vue({
- el: element,
- render(h) {
- return h(ObservabilityApp, {
- props: {
- observabilityIframeSrc: url,
- inlineEmbed: true,
- skeletonVariant: SKELETON_VARIANT_EMBED,
- height: INLINE_EMBED_DIMENSIONS.HEIGHT,
- width: INLINE_EMBED_DIMENSIONS.WIDTH,
- },
- });
- },
- });
-};
-
-export default function renderObservability(elements) {
- return elements.map(mountVueComponent);
-}
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index ce77ede9fe4..6e0b1250479 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -131,10 +131,13 @@ $(document).on('markdown-preview:show', (e, $form) => {
lastTextareaPreviewed = $form.find('textarea.markdown-area');
lastTextareaHeight = lastTextareaPreviewed.height();
- // toggle tabs
- $form.find(previewButtonSelector).val('edit');
- $form.find(previewButtonSelector).children('span.gl-button-text').text(__('Continue editing'));
- $form.find(previewButtonSelector).addClass('gl-shadow-none! gl-bg-transparent!');
+ const $previewButton = $form.find(previewButtonSelector);
+
+ if (!$previewButton.parents('.js-vue-markdown-field').length) {
+ $previewButton.val('edit');
+ $previewButton.children('span.gl-button-text').text(__('Continue editing'));
+ $previewButton.addClass('gl-shadow-none! gl-bg-transparent!');
+ }
// toggle content
$form.find('.md-write-holder').hide();
@@ -154,9 +157,12 @@ $(document).on('markdown-preview:hide', (e, $form) => {
$form.find('textarea.markdown-area').height(lastTextareaHeight);
}
- // toggle tabs
- $form.find(previewButtonSelector).val('preview');
- $form.find(previewButtonSelector).children('span.gl-button-text').text(__('Preview'));
+ const $previewButton = $form.find(previewButtonSelector);
+
+ if (!$previewButton.parents('.js-vue-markdown-field').length) {
+ $previewButton.val('preview');
+ $previewButton.children('span.gl-button-text').text(__('Preview'));
+ }
// toggle content
$form.find('.md-write-holder').show();
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue
index cb9997b7c54..5592a75a4d2 100644
--- a/app/assets/javascripts/blob/components/blob_content.vue
+++ b/app/assets/javascripts/blob/components/blob_content.vue
@@ -98,7 +98,7 @@ export default {
:file-name="blob.name"
:type="activeViewer.fileType"
:hide-line-numbers="hideLineNumbers"
- data-qa-selector="blob_viewer_file_content"
+ data-testid="blob-viewer-file-content"
@richContentLoaded="richContentLoaded = true"
/>
</template>
diff --git a/app/assets/javascripts/blob/components/blob_edit_header.vue b/app/assets/javascripts/blob/components/blob_edit_header.vue
index 8fd3f03ff71..cd2872026c1 100644
--- a/app/assets/javascripts/blob/components/blob_edit_header.vue
+++ b/app/assets/javascripts/blob/components/blob_edit_header.vue
@@ -48,7 +48,7 @@ export default {
variant="danger"
category="secondary"
:disabled="!canDelete"
- data-qa-selector="delete_file_button"
+ data-testid="delete-file-button"
@click="$emit('delete')"
>{{ s__('Snippets|Delete file') }}</gl-button
>
diff --git a/app/assets/javascripts/blob/components/blob_header_default_actions.vue b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
index 12a198f78ea..ddc135e2de7 100644
--- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue
+++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
@@ -96,7 +96,7 @@ export default {
};
</script>
<template>
- <gl-button-group data-qa-selector="default_actions_container">
+ <gl-button-group data-testid="default-actions-container">
<gl-button
v-if="showCopyButton"
v-gl-tooltip.hover
@@ -104,8 +104,7 @@ export default {
:title="$options.BTN_COPY_CONTENTS_TITLE"
:disabled="copyDisabled"
:data-clipboard-target="getBlobHashTarget"
- data-testid="copyContentsButton"
- data-qa-selector="copy_contents_button"
+ data-testid="copy-contents-button"
icon="copy-to-clipboard"
category="primary"
variant="default"
diff --git a/app/assets/javascripts/blob/components/blob_header_filepath.vue b/app/assets/javascripts/blob/components/blob_header_filepath.vue
index 95b88937c32..9187b45788a 100644
--- a/app/assets/javascripts/blob/components/blob_header_filepath.vue
+++ b/app/assets/javascripts/blob/components/blob_header_filepath.vue
@@ -49,7 +49,7 @@ export default {
<file-icon :file-name="fileName" :size="16" aria-hidden="true" css-classes="gl-mr-3" />
<strong
class="file-title-name mr-1 js-blob-header-filepath"
- data-qa-selector="file_title_content"
+ data-testid="file-title-content"
>{{ fileName }}</strong
>
</template>
diff --git a/app/assets/javascripts/blob/csv/constants.js b/app/assets/javascripts/blob/csv/constants.js
new file mode 100644
index 00000000000..7445b653d28
--- /dev/null
+++ b/app/assets/javascripts/blob/csv/constants.js
@@ -0,0 +1 @@
+export const MAX_ROWS_TO_RENDER = 2000;
diff --git a/app/assets/javascripts/blob/csv/csv_viewer.vue b/app/assets/javascripts/blob/csv/csv_viewer.vue
index 169167625e0..7231d023024 100644
--- a/app/assets/javascripts/blob/csv/csv_viewer.vue
+++ b/app/assets/javascripts/blob/csv/csv_viewer.vue
@@ -1,12 +1,15 @@
<script>
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlLoadingIcon, GlTable, GlButton } from '@gitlab/ui';
import Papa from 'papaparse';
+import { setUrlParams } from '~/lib/utils/url_utility';
import PapaParseAlert from '~/vue_shared/components/papa_parse_alert.vue';
+import { MAX_ROWS_TO_RENDER } from './constants';
export default {
components: {
PapaParseAlert,
GlTable,
+ GlButton,
GlLoadingIcon,
},
props: {
@@ -25,8 +28,14 @@ export default {
items: [],
papaParseErrors: [],
loading: true,
+ isTooLarge: false,
};
},
+ computed: {
+ pathToRawFile() {
+ return setUrlParams({ plain: 1 });
+ },
+ },
mounted() {
if (!this.remoteFile) {
const parsed = Papa.parse(this.csv, { skipEmptyLines: true });
@@ -43,7 +52,11 @@ export default {
},
methods: {
handleParsedData(parsed) {
- this.items = parsed.data;
+ if (parsed.data.length > MAX_ROWS_TO_RENDER) {
+ this.isTooLarge = true;
+ }
+
+ this.items = parsed.data.slice(0, MAX_ROWS_TO_RENDER);
if (parsed.errors.length) {
this.papaParseErrors = parsed.errors;
@@ -63,12 +76,28 @@ export default {
<div v-else>
<papa-parse-alert v-if="papaParseErrors.length" :papa-parse-errors="papaParseErrors" />
<gl-table
- :empty-text="__('No CSV data to display.')"
+ :empty-text="s__('CsvViewer|No CSV data to display.')"
:items="items"
:fields="$options.fields"
show-empty
thead-class="gl-display-none"
/>
+ <div
+ v-if="isTooLarge"
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-p-5"
+ >
+ <p data-testid="large-csv-text">
+ {{
+ s__(
+ 'CsvViewer|The file is too large to render all the rows. To see the entire file, switch to the raw view.',
+ )
+ }}
+ </p>
+
+ <gl-button category="secondary" variant="confirm" :href="pathToRawFile">{{
+ s__('CsvViewer|View raw data')
+ }}</gl-button>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
index 1cfa35ffd91..4d915ff341a 100644
--- a/app/assets/javascripts/boards/components/board_app.vue
+++ b/app/assets/javascripts/boards/components/board_app.vue
@@ -1,6 +1,7 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
+import { omit } from 'lodash';
import { refreshCurrentPage, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import BoardContent from '~/boards/components/board_content.vue';
@@ -115,9 +116,8 @@ export default {
return this.activeListId ? this.boardListsApollo[this.activeListId] : undefined;
},
formattedFilterParams() {
- if (this.filterParams.groupBy) delete this.filterParams.groupBy;
return filterVariables({
- filters: this.filterParams,
+ filters: omit(this.filterParams, 'groupBy'),
issuableType: this.issuableType,
filterInfo: FiltersInfo,
filterFields: FilterFields,
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 05865dc7305..fd45d2d31c3 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -2,6 +2,9 @@
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
import Tracking from '~/tracking';
+import setSelectedBoardItemsMutation from '~/boards/graphql/client/set_selected_board_items.mutation.graphql';
+import unsetSelectedBoardItemsMutation from '~/boards/graphql/client/unset_selected_board_items.mutation.graphql';
+import selectedBoardItemsQuery from '~/boards/graphql/client/selected_board_items.query.graphql';
import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import BoardCardInner from './board_card_inner.vue';
@@ -52,9 +55,12 @@ export default {
return !this.isApolloBoard;
},
},
+ selectedBoardItems: {
+ query: selectedBoardItemsQuery,
+ },
},
computed: {
- ...mapState(['selectedBoardItems', 'activeId']),
+ ...mapState(['activeId']),
activeItemId() {
return this.isApolloBoard ? this.activeBoardItem?.id : this.activeId;
},
@@ -62,10 +68,7 @@ export default {
return this.item.id === this.activeItemId;
},
multiSelectVisible() {
- return (
- !this.activeItemId &&
- this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.item.id) > -1
- );
+ return !this.activeItemId && this.selectedBoardItems?.includes(this.item.id);
},
isDisabled() {
return this.disabled || !this.item.id || this.item.isLoading || !this.canAdmin;
@@ -93,7 +96,7 @@ export default {
},
},
methods: {
- ...mapActions(['toggleBoardItemMultiSelection', 'toggleBoardItem']),
+ ...mapActions(['toggleBoardItem']),
toggleIssue(e) {
// Don't do anything if this happened on a no trigger element
if (e.target.closest('.js-no-trigger')) return;
@@ -110,7 +113,10 @@ export default {
this.track('click_card', { label: 'right_sidebar' });
}
},
- toggleItem() {
+ async toggleItem() {
+ await this.$apollo.mutate({
+ mutation: unsetSelectedBoardItemsMutation,
+ });
this.$apollo.mutate({
mutation: setActiveBoardItemMutation,
variables: {
@@ -119,13 +125,32 @@ export default {
},
});
},
+ async toggleBoardItemMultiSelection(item) {
+ if (this.activeItemId) {
+ await this.$apollo.mutate({
+ mutation: setSelectedBoardItemsMutation,
+ variables: {
+ itemId: this.activeItemId,
+ },
+ });
+ await this.$apollo.mutate({
+ mutation: setActiveBoardItemMutation,
+ variables: { boardItem: null },
+ });
+ }
+ this.$apollo.mutate({
+ mutation: setSelectedBoardItemsMutation,
+ variables: {
+ itemId: item.id,
+ },
+ });
+ },
},
};
</script>
<template>
<li
- data-qa-selector="board_card"
:class="[
{
'multi-select gl-bg-blue-50 gl-border-blue-200': multiSelectVisible,
@@ -141,7 +166,7 @@ export default {
:data-item-iid="item.iid"
:data-item-path="item.referencePath"
:style="cardStyle"
- data-testid="board_card"
+ data-testid="board-card"
class="board-card gl-p-5 gl-rounded-base gl-line-height-normal gl-relative gl-mb-3"
@click="toggleIssue($event)"
>
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index c441a718dd8..c10ff2e08da 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -9,11 +9,12 @@ import {
} from '@gitlab/ui';
import { sortBy } from 'lodash';
// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
+import { mapActions } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { sprintf, __, n__ } from '~/locale';
+import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@@ -86,8 +87,13 @@ export default {
maxCounter: 99,
};
},
+ apollo: {
+ isShowingLabels: {
+ query: isShowingLabelsQuery,
+ update: (data) => data.isShowingLabels,
+ },
+ },
computed: {
- ...mapState(['isShowingLabels']),
isLoading() {
return this.item.isLoading || this.item.iid === '-1';
},
@@ -252,7 +258,7 @@ export default {
v-if="item.hidden"
v-gl-tooltip
name="spam"
- :title="__('This issue is hidden because its author has been banned')"
+ :title="__('This issue is hidden because its author has been banned.')"
class="gl-mr-2 hidden-icon gl-text-orange-500 gl-cursor-help"
data-testid="hidden-icon"
/>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index bcd7db8dcb4..67a4c5eba45 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -93,7 +93,7 @@ export default {
}"
:data-list-id="list.id"
class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal is-expandable"
- data-qa-selector="board_list"
+ data-testid="board-list"
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-gray-50"
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 3c2659b00c9..554f3bfa416 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -219,7 +219,7 @@ export default {
<template>
<div
v-cloak
- data-qa-selector="boards_list"
+ data-testid="boards-list"
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-min-h-0"
>
<gl-alert v-if="errorToDisplay" variant="danger" :dismissible="true" @dismiss="dismissError">
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index d12478b42d8..a3d55ac8306 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,13 +1,15 @@
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
+import { mapActions } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { visitUrl, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import eventHub from '~/boards/eventhub';
import { formType } from '../constants';
+import { setError } from '../graphql/cache_updates';
+import errorQuery from '../graphql/client/error.query.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';
@@ -93,8 +95,13 @@ export default {
isLoading: false,
};
},
+ apollo: {
+ error: {
+ query: errorQuery,
+ update: (data) => data.boardsAppError,
+ },
+ },
computed: {
- ...mapState(['error']),
isNewForm() {
return this.currentPage === formType.new;
},
@@ -133,7 +140,7 @@ export default {
variant: this.buttonKind,
disabled: this.submitDisabled,
loading: this.isLoading,
- 'data-qa-selector': 'save_changes_button',
+ 'data-testid': 'save-changes-button',
},
};
},
@@ -177,7 +184,8 @@ export default {
}
},
methods: {
- ...mapActions(['setError', 'unsetError', 'setBoard']),
+ ...mapActions(['setBoard']),
+ setError,
isFocusMode() {
return Boolean(document.querySelector('.content-wrapper > .js-focus-mode-board.is-focused'));
},
@@ -211,8 +219,8 @@ export default {
try {
await this.deleteBoard();
visitUrl(this.boardBaseUrl);
- } catch {
- this.setError({ message: this.$options.i18n.deleteErrorMessage });
+ } catch (error) {
+ setError({ error, message: this.$options.i18n.deleteErrorMessage });
} finally {
this.isLoading = false;
}
@@ -236,8 +244,8 @@ export default {
: '';
updateHistory({ url: `${this.boardBaseUrl}/${getIdFromGraphQLId(board.id)}${param}` });
}
- } catch {
- this.setError({ message: this.$options.i18n.saveErrorMessage });
+ } catch (error) {
+ setError({ error, message: this.$options.i18n.saveErrorMessage });
} finally {
this.isLoading = false;
}
@@ -295,11 +303,11 @@ export default {
@hide.prevent
>
<gl-alert
- v-if="!isApolloBoard && error"
+ v-if="error"
class="gl-mb-3"
variant="danger"
:dismissible="true"
- @dismiss="unsetError"
+ @dismiss="() => setError({ message: null, captureError: false })"
>
{{ error }}
</gl-alert>
@@ -316,7 +324,7 @@ export default {
ref="name"
v-model="board.name"
class="form-control"
- data-qa-selector="board_name_field"
+ data-testid="board-name-field"
type="text"
:placeholder="$options.i18n.titleFieldPlaceholder"
@keyup.enter="submit"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 1bb7e88122a..2693a6bb5ea 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -653,7 +653,7 @@ export default {
<div
v-show="!list.collapsed"
class="board-list-component gl-relative gl-h-full gl-display-flex gl-flex-direction-column gl-min-h-0"
- data-qa-selector="board_list_cards_area"
+ data-testid="board-list-cards-area"
>
<div
v-if="loading"
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 42c30dc8245..0235edd69ac 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -201,8 +201,8 @@ export default {
})
);
},
- totalWeight() {
- return this.boardList?.totalWeight;
+ totalIssueWeight() {
+ return this.boardList?.totalIssueWeight;
},
canShowTotalWeight() {
return this.weightFeatureAvailable && !this.isLoading;
@@ -365,7 +365,6 @@ export default {
}"
:style="headerStyle"
class="board-header gl-relative"
- data-qa-selector="board_list_header"
data-testid="board-list-header"
>
<h3
@@ -473,8 +472,8 @@ export default {
<div v-else>• {{ itemsTooltipLabel }}</div>
<div v-if="weightFeatureAvailable && !isLoading">
•
- <gl-sprintf :message="__('%{totalWeight} total weight')">
- <template #totalWeight>{{ totalWeight }}</template>
+ <gl-sprintf :message="__('%{totalIssueWeight} total weight')">
+ <template #totalIssueWeight>{{ totalIssueWeight }}</template>
</gl-sprintf>
</div>
</gl-tooltip>
@@ -507,7 +506,7 @@ export default {
<gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" />
<span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3" data-testid="weight">
<gl-icon class="gl-mr-2" name="weight" :size="14" />
- {{ totalWeight }}
+ {{ totalIssueWeight }}
</span>
</template>
<!-- EE end -->
diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue
index 7fd1a934381..31664c28831 100644
--- a/app/assets/javascripts/boards/components/board_top_bar.vue
+++ b/app/assets/javascripts/boards/components/board_top_bar.vue
@@ -4,6 +4,7 @@ import { s__ } from '~/locale';
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue';
import { getBoardQuery } from 'ee_else_ce/boards/boards_util';
+import ToggleLabels from '~/vue_shared/components/toggle_labels.vue';
import { setError } from '../graphql/cache_updates';
import ConfigToggle from './config_toggle.vue';
import NewBoardButton from './new_board_button.vue';
@@ -17,7 +18,7 @@ export default {
ConfigToggle,
NewBoardButton,
ToggleFocus,
- ToggleLabels: () => import('ee_component/boards/components/toggle_labels.vue'),
+ ToggleLabels,
ToggleEpicsSwimlanes: () => import('ee_component/boards/components/toggle_epics_swimlanes.vue'),
EpicBoardFilteredSearch: () =>
import('ee_component/boards/components/epic_filtered_search.vue'),
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index cc6fde92f9b..cd2a4a02b2e 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -1,15 +1,7 @@
<script>
-import {
- GlLoadingIcon,
- GlSearchBoxByType,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlModalDirective,
-} from '@gitlab/ui';
+import { GlButton, GlCollapsibleListbox, GlModalDirective } from '@gitlab/ui';
import { produce } from 'immer';
-import { throttle } from 'lodash';
+import { differenceBy, debounce } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
@@ -18,7 +10,8 @@ import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isMetaKey } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { s__, __ } from '~/locale';
import eventHub from '../eventhub';
import groupBoardsQuery from '../graphql/group_boards.query.graphql';
@@ -34,15 +27,16 @@ export default {
name: 'BoardsSelector',
i18n: {
fetchBoardsError: s__('Boards|An error occurred while fetching boards. Please try again.'),
+ headerText: s__('IssueBoards|Switch board'),
+ noResultsText: s__('IssueBoards|No matching boards found'),
+ hiddenBoardsText: s__(
+ 'IssueBoards|Some of your boards are hidden, add a license to see them again.',
+ ),
},
components: {
BoardForm,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
+ GlButton,
+ GlCollapsibleListbox,
},
directives: {
GlModalDirective,
@@ -60,11 +54,6 @@ export default {
'isApolloBoard',
],
props: {
- throttleDuration: {
- type: Number,
- default: 200,
- required: false,
- },
boardApollo: {
type: Object,
required: false,
@@ -78,13 +67,10 @@ export default {
},
data() {
return {
- hasScrollFade: false,
- scrollFadeInitialized: false,
boards: [],
recentBoards: [],
loadingBoards: false,
loadingRecentBoards: false,
- throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
filterTerm: '',
@@ -97,6 +83,12 @@ export default {
boardToUse() {
return this.isApolloBoard ? this.boardApollo : this.board;
},
+ boardToUseName() {
+ return this.boardToUse?.name || s__('IssueBoards|Select board');
+ },
+ boardToUseId() {
+ return getIdFromGraphQLId(this.boardToUse.id) || '';
+ },
isBoardToUseLoading() {
return this.isApolloBoard ? this.isCurrentBoardLoading : this.isBoardLoading;
},
@@ -112,6 +104,26 @@ export default {
loading() {
return this.loadingRecentBoards || this.loadingBoards;
},
+ listBoxItems() {
+ const mapItems = ({ id, name }) => ({ text: name, value: id });
+
+ if (this.showRecentSection) {
+ const notRecent = differenceBy(this.filteredBoards, this.recentBoards, 'id');
+
+ return [
+ {
+ text: __('Recent'),
+ options: this.recentBoards.map(mapItems),
+ },
+ {
+ text: __('All'),
+ options: notRecent.map(mapItems),
+ },
+ ];
+ }
+
+ return this.filteredBoards.map(mapItems);
+ },
filteredBoards() {
return this.boards.filter((board) =>
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
@@ -126,34 +138,25 @@ export default {
showDropdown() {
return this.showCreate || this.hasMissingBoards;
},
- scrollFadeClass() {
- return {
- 'fade-out': !this.hasScrollFade,
- };
- },
showRecentSection() {
return (
- this.recentBoards.length &&
+ this.recentBoards.length > 0 &&
this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
!this.filterTerm.length
);
},
},
watch: {
- filteredBoards() {
- this.scrollFadeInitialized = false;
- this.$nextTick(this.setScrollFade);
- },
- recentBoards() {
- this.scrollFadeInitialized = false;
- this.$nextTick(this.setScrollFade);
- },
boardToUse(newBoard) {
document.title = newBoard.name;
},
},
created() {
eventHub.$on('showBoardModal', this.showPage);
+ this.handleSearch = debounce(this.setFilterTerm, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ destroyed() {
+ this.handleSearch.cancel();
},
beforeDestroy() {
eventHub.$off('showBoardModal', this.showPage);
@@ -248,34 +251,6 @@ export default {
this.$emit('switchBoard', board.id);
},
- 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();
- },
fetchCurrentBoard(boardId) {
this.fetchBoard({
fullPath: this.fullPath,
@@ -283,17 +258,24 @@ export default {
boardType: this.boardType,
});
},
- async switchBoard(boardId, e) {
+ setFilterTerm(value) {
+ this.filterTerm = value;
+ },
+ async switchBoardKeyEvent(boardId, e) {
if (isMetaKey(e)) {
+ e.stopPropagation();
window.open(`${this.boardBaseUrl}/${boardId}`, '_blank');
- } else if (this.isApolloBoard) {
+ }
+ },
+ switchBoardGroup(value) {
+ if (this.isApolloBoard) {
// Epic board ID is supported in EE version of this file
- this.$emit('switchBoard', this.fullBoardId(boardId));
- updateHistory({ url: `${this.boardBaseUrl}/${boardId}` });
+ this.$emit('switchBoard', this.fullBoardId(value));
+ updateHistory({ url: `${this.boardBaseUrl}/${value}` });
} else {
this.unsetActiveId();
- this.fetchCurrentBoard(boardId);
- updateHistory({ url: `${this.boardBaseUrl}/${boardId}` });
+ this.fetchCurrentBoard(value);
+ updateHistory({ url: `${this.boardBaseUrl}/${value}` });
}
},
},
@@ -303,105 +285,65 @@ export default {
<template>
<div class="boards-switcher gl-mr-3" data-testid="boards-selector">
<span class="boards-selector-wrapper">
- <gl-dropdown
+ <gl-collapsible-listbox
v-if="showDropdown"
+ block
data-testid="boards-dropdown"
- data-qa-selector="boards_dropdown"
- toggle-class="dropdown-menu-toggle"
- menu-class="flex-column dropdown-extended-height"
+ searchable
+ :searching="loading"
+ toggle-class="gl-min-w-20"
+ :header-text="$options.i18n.headerText"
+ :no-results-text="$options.i18n.noResultsText"
:loading="isBoardToUseLoading"
- :text="boardToUse.name"
- @show="loadBoards"
+ :items="listBoxItems"
+ :toggle-text="boardToUseName"
+ :selected="boardToUseId"
+ @search="handleSearch"
+ @select="switchBoardGroup"
+ @shown="loadBoards"
>
- <p class="gl-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}`"
- data-testid="dropdown-item"
- @click.prevent="switchBoard(recentBoard.id, $event)"
- >
- {{ 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"
- data-testid="dropdown-item"
- @click.prevent="switchBoard(otherBoard.id, $event)"
- >
- {{ otherBoard.name }}
- </gl-dropdown-item>
-
- <gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events">
+ <template #list-item="{ item }">
+ <div data-testid="dropdown-item-recent" @click="switchBoardKeyEvent(item.value, $event)">
+ {{ item.text }}
+ </div>
+ </template>
+
+ <template #footer>
+ <div v-if="hasMissingBoards" class="gl-border-t gl-font-sm gl-px-4 gl-pt-4 gl-pb-3">
{{
s__('IssueBoards|Some of your boards are hidden, add 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" size="sm" />
-
- <div v-if="canAdminBoard">
- <gl-dropdown-divider />
-
- <gl-dropdown-item
- v-if="showCreate"
- v-gl-modal-directive="'board-config-modal'"
- data-qa-selector="create_new_board_button"
- data-track-action="click_button"
- data-track-label="create_new_board"
- data-track-property="dropdown"
- @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"
- @click.prevent="showPage('delete')"
- >
- {{ s__('IssueBoards|Delete board') }}
- </gl-dropdown-item>
- </div>
- </gl-dropdown>
+ </div>
+ <div v-if="canAdminBoard" class="gl-border-t gl-py-2 gl-px-2">
+ <gl-button
+ v-if="showCreate"
+ v-gl-modal-directive="'board-config-modal'"
+ block
+ class="gl-justify-content-start!"
+ category="tertiary"
+ data-testid="create-new-board-button"
+ data-track-action="click_button"
+ data-track-label="create_new_board"
+ data-track-property="dropdown"
+ @click="showPage('new')"
+ >
+ {{ s__('IssueBoards|Create new board') }}
+ </gl-button>
+
+ <gl-button
+ v-if="showDelete"
+ v-gl-modal-directive="'board-config-modal'"
+ block
+ category="tertiary"
+ variant="danger"
+ class="gl-mt-0! gl-justify-content-start!"
+ @click="showPage('delete')"
+ >
+ {{ s__('IssueBoards|Delete board') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-collapsible-listbox>
<board-form
v-if="currentPage"
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index bc896932ffc..69e6cc870d2 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -49,7 +49,7 @@ export default {
v-gl-tooltip
:title="tooltipTitle"
:class="{ 'dot-highlight': hasScope || boardHasScope }"
- data-qa-selector="boards_config_button"
+ data-testid="boards-config-button"
@click.prevent="showPage"
>
{{ buttonText }}
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index a7b3f5536a4..c28415de620 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -62,11 +62,7 @@ export default {
tokensCE() {
const { issue, incident } = this.$options.i18n;
const { types } = this.$options;
- const { fetchUsers, fetchLabels } = issueBoardFilters(
- this.$apollo,
- this.fullPath,
- this.isGroupBoard,
- );
+ const { fetchLabels } = issueBoardFilters(this.$apollo, this.fullPath, this.isGroupBoard);
const tokens = [
{
@@ -77,7 +73,8 @@ export default {
token: UserToken,
dataType: 'user',
unique: true,
- fetchUsers,
+ isProject: !this.isGroupBoard,
+ fullPath: this.fullPath,
preloadedUsers: this.preloadedUsers(),
},
{
@@ -89,7 +86,8 @@ export default {
token: UserToken,
dataType: 'user',
unique: true,
- fetchUsers,
+ isProject: !this.isGroupBoard,
+ fullPath: this.fullPath,
preloadedUsers: this.preloadedUsers(),
},
{
diff --git a/app/assets/javascripts/boards/components/toggle_focus.vue b/app/assets/javascripts/boards/components/toggle_focus.vue
index 990a6fa63d4..a886abf9e61 100644
--- a/app/assets/javascripts/boards/components/toggle_focus.vue
+++ b/app/assets/javascripts/boards/components/toggle_focus.vue
@@ -38,7 +38,7 @@ export default {
v-gl-tooltip
category="tertiary"
:icon="isFullscreen ? 'minimize' : 'maximize'"
- data-qa-selector="focus_mode_button"
+ data-testid="focus-mode-button"
:title="$options.i18n.toggleFocusMode"
:aria-label="$options.i18n.toggleFocusMode"
@click="toggleFocusMode"
diff --git a/app/assets/javascripts/boards/graphql/cache_updates.js b/app/assets/javascripts/boards/graphql/cache_updates.js
index 3551c3ed982..ea099e02181 100644
--- a/app/assets/javascripts/boards/graphql/cache_updates.js
+++ b/app/assets/javascripts/boards/graphql/cache_updates.js
@@ -68,7 +68,7 @@ export function updateIssueCountAndWeight({
boardList: {
...boardList,
issuesCount: boardList.issuesCount - 1,
- totalWeight: boardList.totalWeight - issue.weight,
+ totalIssueWeight: boardList.totalIssueWeight - issue.weight,
},
}),
);
@@ -83,7 +83,7 @@ export function updateIssueCountAndWeight({
boardList: {
...boardList,
issuesCount: boardList.issuesCount + 1,
- totalWeight: boardList.totalWeight + issue.weight,
+ ...(issue.weight ? { totalIssueWeight: boardList.totalIssueWeight + issue.weight } : {}),
},
}),
);
diff --git a/app/assets/javascripts/boards/graphql/client/selected_board_items.query.graphql b/app/assets/javascripts/boards/graphql/client/selected_board_items.query.graphql
new file mode 100644
index 00000000000..88006750221
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/selected_board_items.query.graphql
@@ -0,0 +1,3 @@
+query selectedBoardItems {
+ selectedBoardItems @client
+}
diff --git a/app/assets/javascripts/boards/graphql/client/set_selected_board_items.mutation.graphql b/app/assets/javascripts/boards/graphql/client/set_selected_board_items.mutation.graphql
new file mode 100644
index 00000000000..28274de6c3f
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/set_selected_board_items.mutation.graphql
@@ -0,0 +1,3 @@
+mutation setSelectedBoardItems($itemId: ID!) {
+ setSelectedBoardItems(itemId: $itemId) @client
+}
diff --git a/app/assets/javascripts/boards/graphql/client/unset_selected_board_items.mutation.graphql b/app/assets/javascripts/boards/graphql/client/unset_selected_board_items.mutation.graphql
new file mode 100644
index 00000000000..ab34cf48609
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/client/unset_selected_board_items.mutation.graphql
@@ -0,0 +1,3 @@
+mutation unsetSelectedBoardItems {
+ unsetSelectedBoardItems @client
+}
diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js
index ba5da70c6ec..0a6580dd49b 100644
--- a/app/assets/javascripts/boards/issue_board_filters.js
+++ b/app/assets/javascripts/boards/issue_board_filters.js
@@ -1,5 +1,3 @@
-import { BoardType } from 'ee_else_ce/boards/constants';
-import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import boardLabels from './graphql/board_labels.query.graphql';
export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
@@ -7,17 +5,6 @@ export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
return isGroupBoard ? data.group?.labels.nodes || [] : data.project?.labels.nodes || [];
};
- const fetchUsers = (usersSearchTerm) => {
- const namespace = isGroupBoard ? BoardType.group : BoardType.project;
-
- return apollo
- .query({
- query: usersAutocompleteQuery,
- variables: { fullPath, search: usersSearchTerm, isProject: !isGroupBoard },
- })
- .then(({ data }) => data[namespace]?.autocompleteUsers);
- };
-
const fetchLabels = (labelSearchTerm) => {
return apollo
.query({
@@ -34,6 +21,5 @@ export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
return {
fetchLabels,
- fetchUsers,
};
}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index e044283534a..3e7d7a7a8d3 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -621,7 +621,7 @@ export default {
__typename: 'BoardList',
id: fromList.boardList.id,
issuesCount: fromList.boardList.issuesCount - 1,
- totalWeight: fromList.boardList.totalWeight - Number(weight),
+ totalIssueWeight: fromList.boardList.totalIssueWeight - Number(weight),
},
};
@@ -645,7 +645,7 @@ export default {
__typename: 'BoardList',
id: toList.boardList.id,
issuesCount: toList.boardList.issuesCount + 1,
- totalWeight: toList.boardList.totalWeight + Number(weight),
+ totalIssueWeight: toList.boardList.totalIssueWeight + Number(weight),
},
};
@@ -731,7 +731,7 @@ export default {
__typename: 'BoardList',
id: fromList.boardList.id,
issuesCount: fromList.boardList.issuesCount + 1,
- totalWeight: fromList.boardList.totalWeight,
+ totalIssueWeight: fromList.boardList.totalIssueWeight,
},
};
diff --git a/app/assets/javascripts/branches/components/delete_merged_branches.vue b/app/assets/javascripts/branches/components/delete_merged_branches.vue
index 50fe610d335..24ae9b83b9c 100644
--- a/app/assets/javascripts/branches/components/delete_merged_branches.vue
+++ b/app/assets/javascripts/branches/components/delete_merged_branches.vue
@@ -1,5 +1,12 @@
<script>
-import { GlDisclosureDropdown, GlButton, GlFormInput, GlModal, GlSprintf } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlButton,
+ GlFormInput,
+ GlModal,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { sprintf, s__, __ } from '~/locale';
@@ -22,6 +29,7 @@ export const i18n = {
'Branches|Plese type the following to confirm: %{codeStart}delete%{codeEnd}.',
),
cancelButtonText: __('Cancel'),
+ actionsToggleText: __('More actions'),
};
export default {
@@ -33,6 +41,9 @@ export default {
GlFormInput,
GlSprintf,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
formPath: {
type: String,
@@ -96,6 +107,10 @@ export default {
<template>
<div>
<gl-disclosure-dropdown
+ v-gl-tooltip.hover.top="{
+ title: $options.i18n.actionsToggleText,
+ boundary: 'viewport',
+ }"
:toggle-text="$options.i18n.actionsToggleText"
text-sr-only
icon="ellipsis_v"
@@ -153,7 +168,7 @@ export default {
<gl-form-input
v-model="enteredText"
type="text"
- size="sm"
+ width="sm"
class="gl-mt-2"
aria-labelledby="input-label"
autocomplete="off"
diff --git a/app/assets/javascripts/branches/components/sort_dropdown.vue b/app/assets/javascripts/branches/components/sort_dropdown.vue
index 4866d506988..f4a5b25c4f2 100644
--- a/app/assets/javascripts/branches/components/sort_dropdown.vue
+++ b/app/assets/javascripts/branches/components/sort_dropdown.vue
@@ -1,6 +1,6 @@
<script>
import { GlCollapsibleListbox, GlSearchBoxByClick } from '@gitlab/ui';
-import { mergeUrlParams, visitUrl } from '~/lib/utils/url_utility';
+import { mergeUrlParams, visitUrl, getParameterValues } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
export default {
@@ -21,7 +21,7 @@ export default {
// own attributes, also in created()
data() {
return {
- searchTerm: '',
+ searchTerm: getParameterValues('search')[0] || '',
};
},
computed: {
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
index cbb80a5175f..9d516fc267d 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
@@ -23,6 +23,6 @@ export default {
</script>
<template>
<div class="gl-text-truncate">
- <gl-link :href="projectUrl"> {{ projectName }}</gl-link>
+ <gl-link :href="projectUrl" data-testid="job-project-link">{{ projectName }}</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
index a76829aa129..e44f756a5c5 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
@@ -1,5 +1,6 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+import RunnerTypeIcon from '~/ci/runner/components/runner_type_icon.vue';
import { RUNNER_EMPTY_TEXT, RUNNER_NO_DESCRIPTION } from '../../constants';
export default {
@@ -9,6 +10,10 @@ export default {
},
components: {
GlLink,
+ RunnerTypeIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
props: {
job: {
@@ -25,15 +30,19 @@ export default {
? this.job.runner.description
: this.$options.i18n.noRunnerDescription;
},
+ runnerType() {
+ return this.job.runner?.runnerType;
+ },
},
};
</script>
<template>
<div class="gl-text-truncate">
- <gl-link v-if="adminUrl" :href="adminUrl">
- {{ description }}
- </gl-link>
+ <span v-if="adminUrl">
+ <runner-type-icon :type="runnerType" class="gl-vertical-align-middle" />
+ <gl-link :href="adminUrl" data-testid="job-runner-link"> {{ description }} </gl-link>
+ </span>
<span v-else data-testid="empty-runner-text"> {{ $options.i18n.emptyRunnerText }}</span>
</div>
</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/constants.js b/app/assets/javascripts/ci/admin/jobs_table/constants.js
index ff0efdb1f5b..86c9ab53e75 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/constants.js
+++ b/app/assets/javascripts/ci/admin/jobs_table/constants.js
@@ -26,9 +26,6 @@ export const DEFAULT_FIELDS_ADMIN = [
{ key: 'project', label: __('Project'), columnClass: 'gl-w-20p' },
{ key: 'runner', label: __('Runner'), columnClass: 'gl-w-15p' },
{ key: 'pipeline', label: __('Pipeline'), columnClass: 'gl-w-10p' },
- { key: 'stage', label: __('Stage'), columnClass: 'gl-w-10p' },
- { key: 'name', label: __('Name'), columnClass: 'gl-w-15p' },
- { key: 'duration', label: __('Duration'), columnClass: 'gl-w-15p' },
{ key: 'actions', label: '', columnClass: 'gl-w-10p' },
];
diff --git a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
index 89fb1782e46..2e77f4db907 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
@@ -16,6 +16,7 @@ query getAllJobs(
id
description
adminUrl
+ runnerType
}
artifacts {
nodes {
diff --git a/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue b/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue
index 00f5b2eab7d..c27ec0dd500 100644
--- a/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue
+++ b/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue
@@ -65,6 +65,7 @@ export default {
:title="$options.i18n.modalTitle(checkedCount)"
:action-primary="modalActionPrimary"
:action-cancel="modalActionCancel"
+ data-testid="artifacts-bulk-delete-modal"
v-bind="$attrs"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
index e08470c62be..d8f9eb65236 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -5,16 +5,15 @@ import {
GlLink,
GlButtonGroup,
GlButton,
- GlBadge,
GlIcon,
GlPagination,
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql';
import { totalArtifactsSizeForJob, mapArchivesToJobNodes, mapBooleansToJobNodes } from '../utils';
@@ -65,12 +64,11 @@ export default {
GlLink,
GlButtonGroup,
GlButton,
- GlBadge,
GlIcon,
GlPagination,
GlFormCheckbox,
- CiIcon,
TimeAgo,
+ CiBadgeLink,
JobCheckbox,
ArtifactsBulkDelete,
BulkDeleteModal,
@@ -328,7 +326,7 @@ export default {
{
key: 'artifacts',
label: I18N_ARTIFACTS,
- thClass: 'gl-w-quarter',
+ thClass: 'gl-w-eighth',
},
{
key: 'job',
@@ -350,7 +348,7 @@ export default {
{
key: 'actions',
label: '',
- thClass: 'gl-w-eighth',
+ thClass: 'gl-w-20p',
tdClass: 'gl-text-right',
},
],
@@ -403,6 +401,7 @@ export default {
:checked="isAnyVisibleArtifactSelected"
:indeterminate="isAnyVisibleArtifactSelected && !areAllVisibleArtifactsSelected"
:disabled="isSelectedArtifactsLimitReached && !isAnyVisibleArtifactSelected"
+ data-testid="select-all-artifacts-checkbox"
@change="handleSelectAllChecked"
/>
</template>
@@ -441,45 +440,37 @@ export default {
</span>
</template>
<template #cell(job)="{ item }">
- <span class="gl-display-inline-flex gl-align-items-center gl-w-full gl-mb-4">
+ <div class="gl-display-inline-flex gl-align-items-center gl-mb-3 gl-gap-3">
<span data-testid="job-artifacts-job-status">
- <ci-icon v-if="item.succeeded" :status="item.detailedStatus" class="gl-mr-3" />
- <gl-badge
- v-else
- :icon="item.detailedStatus.icon"
- :variant="$options.STATUS_BADGE_VARIANTS[item.detailedStatus.group]"
- class="gl-mr-3"
- >
- {{ item.detailedStatus.label }}
- </gl-badge>
+ <ci-badge-link :status="item.detailedStatus" size="sm" :show-text="false" />
</span>
- <gl-link :href="item.webPath" class="gl-font-weight-bold">
+ <gl-link :href="item.webPath">
{{ item.name }}
</gl-link>
- </span>
- <span class="gl-display-inline-flex">
+ </div>
+ <div class="gl-mb-1">
<gl-icon name="pipeline" class="gl-mr-2" />
- <gl-link
- :href="item.pipeline.path"
- class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
- >
+ <gl-link :href="item.pipeline.path" class="gl-mr-2">
{{ pipelineId(item) }}
</gl-link>
- <gl-icon name="branch" class="gl-mr-2" />
- <gl-link
- :href="item.refPath"
- class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
- >
- {{ item.refName }}
- </gl-link>
- <gl-icon name="commit" class="gl-mr-2" />
- <gl-link
- :href="item.commitPath"
- class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
- >
- {{ item.shortSha }}
- </gl-link>
- </span>
+ <span class="gl-display-inline-block gl-rounded-base gl-px-2 gl-bg-gray-50">
+ <gl-icon name="commit" :size="12" class="gl-mr-2" />
+ <gl-link
+ :href="item.commitPath"
+ class="gl-text-black-normal gl-font-sm gl-font-monospace"
+ >
+ {{ item.shortSha }}
+ </gl-link>
+ </span>
+ </div>
+ <div>
+ <span class="gl-display-inline-block gl-rounded-base gl-px-2 gl-bg-gray-50">
+ <gl-icon name="branch" :size="12" class="gl-mr-1" />
+ <gl-link :href="item.refPath" class="gl-text-black-normal gl-font-sm gl-font-monospace">
+ {{ item.refName }}
+ </gl-link>
+ </span>
+ </div>
</template>
<template #cell(size)="{ item }">
<span data-testid="job-artifacts-size">{{ artifactsSize(item) }}</span>
diff --git a/app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue b/app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue
new file mode 100644
index 00000000000..5fe7e8286ec
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue
@@ -0,0 +1,8 @@
+<script>
+export default {};
+</script>
+<template>
+ <div>
+ <router-view />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
new file mode 100644
index 00000000000..572a8183730
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
@@ -0,0 +1,120 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { n__, s__, sprintf } from '~/locale';
+import { formatDate } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ isLoadingDetails: {
+ type: Boolean,
+ required: true,
+ },
+ isLoadingSharedData: {
+ type: Boolean,
+ required: true,
+ },
+ openIssuesCount: {
+ required: false,
+ type: Number,
+ default: 0,
+ },
+ openMergeRequestsCount: {
+ required: false,
+ type: Number,
+ default: 0,
+ },
+ latestVersion: {
+ required: false,
+ type: Object,
+ default: () => ({}),
+ },
+ webPath: {
+ required: false,
+ type: String,
+ default: '',
+ },
+ },
+ computed: {
+ hasVersion() {
+ return this.latestVersion;
+ },
+ lastReleaseText() {
+ if (this.hasVersion) {
+ return sprintf(this.$options.i18n.lastRelease, {
+ date: this.releasedAt,
+ });
+ }
+
+ return this.$options.i18n.lastReleaseMissing;
+ },
+ openIssuesText() {
+ return n__('%d issue', '%d issues', this.openIssuesCount);
+ },
+ openMergeRequestText() {
+ return n__('%d merge request', '%d merge requests', this.openMergeRequestsCount);
+ },
+ releasedAt() {
+ return this.hasVersion && formatDate(this.latestVersion.releasedAt, 'yyyy-mm-dd');
+ },
+ projectInfoItems() {
+ return [
+ {
+ icon: 'project',
+ link: `${this.webPath}`,
+ text: this.$options.i18n.projectLink,
+ isLoading: this.isLoadingSharedData,
+ },
+ {
+ icon: 'issues',
+ link: `${this.webPath}/issues`,
+ text: this.openIssuesText,
+ isLoading: this.isLoadingDetails,
+ },
+ {
+ icon: 'merge-request',
+ link: `${this.webPath}/merge_requests`,
+ text: this.openMergeRequestText,
+ isLoading: this.isLoadingDetails,
+ },
+ {
+ icon: 'clock',
+ text: this.lastReleaseText,
+ isLoading: this.isLoadingSharedData,
+ },
+ ];
+ },
+ },
+ i18n: {
+ projectLink: s__('CiCatalog|Go to the project'),
+ lastRelease: s__('CiCatalog|Last release at %{date}'),
+ lastReleaseMissing: s__('CiCatalog|No release available'),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-2 gl-sm-display-flex gl-gap-5">
+ <span
+ v-for="item in projectInfoItems"
+ :key="`${item.icon}`"
+ class="gl-display-flex gl-align-items-center gl-xs-mb-3"
+ >
+ <gl-icon class="gl-text-primary gl-mr-2" :name="item.icon" />
+ <div
+ v-if="item.isLoading"
+ class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-w-15"
+ data-testid="skeleton-loading-line"
+ ></div>
+ <template v-else>
+ <gl-link v-if="item.link" :href="item.link"> {{ item.text }} </gl-link>
+ <span v-else class="gl-text-secondary">
+ {{ item.text }}
+ </span>
+ </template>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
new file mode 100644
index 00000000000..85dfa12c756
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
@@ -0,0 +1,103 @@
+<script>
+import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __, s__ } from '~/locale';
+import getCiCatalogResourceComponents from '../../graphql/queries/get_ci_catalog_resource_components.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTableLite,
+ },
+ props: {
+ resourceId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ components: [],
+ };
+ },
+ apollo: {
+ components: {
+ query: getCiCatalogResourceComponents,
+ variables() {
+ return {
+ id: this.resourceId,
+ };
+ },
+ update(data) {
+ return data?.ciCatalogResource?.components?.nodes || [];
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.components.loading;
+ },
+ },
+ methods: {
+ generateSnippet(componentPath) {
+ // This is not to be translated because it is our CI yaml syntax.
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `include:
+ - component: ${componentPath}`;
+ },
+ humanizeBoolean(bool) {
+ return bool ? __('Yes') : __('No');
+ },
+ },
+ fields: [
+ {
+ key: 'name',
+ label: s__('CiCatalogComponent|Parameters'),
+ thClass: 'gl-w-40p',
+ },
+ {
+ key: 'defaultValue',
+ label: s__('CiCatalogComponent|Default Value'),
+ thClass: 'gl-w-40p',
+ },
+ {
+ key: 'required',
+ label: s__('CiCatalogComponent|Mandatory'),
+ thClass: 'gl-w-20p',
+ },
+ ],
+ i18n: {
+ inputTitle: s__('CiCatalogComponent|Inputs'),
+ fetchError: s__("CiCatalogComponent|There was an error fetching this resource's components"),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" size="lg" />
+ <template v-else>
+ <div
+ v-for="component in components"
+ :key="component.id"
+ class="gl-mb-8"
+ data-testid="component-section"
+ >
+ <h3 class="gl-font-size-h2" data-testid="component-name">{{ component.name }}</h3>
+ <p class="gl-mt-5">{{ component.description }}</p>
+ <pre class="gl-w-85p gl-py-4">{{ generateSnippet(component.path) }}</pre>
+ <div class="gl-mt-5">
+ <b class="gl-display-block gl-mb-4"> {{ $options.i18n.inputTitle }}</b>
+ <gl-table-lite :items="component.inputs.nodes" :fields="$options.fields">
+ <template #cell(required)="{ item }">
+ {{ humanizeBoolean(item.required) }}
+ </template>
+ </gl-table-lite>
+ </div>
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
new file mode 100644
index 00000000000..c0feb52c185
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlTab, GlTabs } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import CiResourceComponents from './ci_resource_components.vue';
+import CiResourceReadme from './ci_resource_readme.vue';
+
+export default {
+ components: {
+ CiResourceReadme,
+ CiResourceComponents,
+ GlTab,
+ GlTabs,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ resourceId: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ tabs: {
+ components: s__('CiCatalog|Components'),
+ readme: s__('CiCatalog|Readme'),
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-tabs>
+ <gl-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
+ <ci-resource-components :resource-id="resourceId"
+ /></gl-tab>
+ <gl-tab :title="$options.i18n.tabs.readme" lazy>
+ <ci-resource-readme :resource-id="resourceId" />
+ </gl-tab>
+ </gl-tabs>
+</template>
+<style></style>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
new file mode 100644
index 00000000000..6673785ffd2
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
@@ -0,0 +1,130 @@
+<script>
+import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isNumeric } from '~/lib/utils/number_utils';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiResourceAbout from './ci_resource_about.vue';
+import CiResourceHeaderSkeletonLoader from './ci_resource_header_skeleton_loader.vue';
+
+export default {
+ components: {
+ CiBadgeLink,
+ CiResourceAbout,
+ CiResourceHeaderSkeletonLoader,
+ GlAvatar,
+ GlAvatarLink,
+ GlBadge,
+ },
+ props: {
+ isLoadingDetails: {
+ type: Boolean,
+ required: true,
+ },
+ isLoadingSharedData: {
+ type: Boolean,
+ required: true,
+ },
+ openIssuesCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ openMergeRequestsCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ pipelineStatus: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ resource: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ entityId() {
+ return getIdFromGraphQLId(this.resource.id);
+ },
+ fullPath() {
+ return `${this.rootNamespace.fullPath}/${this.rootNamespace.name}`;
+ },
+ hasLatestVersion() {
+ return this.latestVersion?.tagName;
+ },
+ hasPipelineStatus() {
+ return this.pipelineStatus?.text;
+ },
+ latestVersion() {
+ return this.resource.latestVersion;
+ },
+ rootNamespace() {
+ return this.resource.rootNamespace;
+ },
+ versionBadgeText() {
+ return isNumeric(this.latestVersion.tagName)
+ ? `v${this.latestVersion.tagName}`
+ : this.latestVersion.tagName;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <ci-resource-header-skeleton-loader v-if="isLoadingSharedData" class="gl-py-5" />
+ <div v-else class="gl-display-flex gl-py-5">
+ <gl-avatar-link :href="resource.webPath">
+ <gl-avatar
+ class="gl-mr-4"
+ :entity-id="entityId"
+ :entity-name="resource.name"
+ shape="rect"
+ :size="64"
+ :src="resource.icon"
+ />
+ </gl-avatar-link>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-align-items-flex-start gl-justify-content-center"
+ >
+ <div class="gl-font-sm gl-text-secondary">
+ {{ fullPath }}
+ </div>
+ <span class="gl-display-flex">
+ <div class="gl-font-lg gl-font-weight-bold">{{ resource.name }}</div>
+ <gl-badge
+ v-if="hasLatestVersion"
+ size="sm"
+ class="gl-ml-3 gl-my-1"
+ :href="latestVersion.tagPath"
+ >
+ {{ versionBadgeText }}
+ </gl-badge>
+ </span>
+ <ci-badge-link
+ v-if="hasPipelineStatus"
+ class="gl-mt-2"
+ :status="pipelineStatus"
+ size="sm"
+ show-text
+ />
+ </div>
+ </div>
+ <ci-resource-about
+ :is-loading-details="isLoadingDetails"
+ :is-loading-shared-data="isLoadingSharedData"
+ :open-issues-count="openIssuesCount"
+ :open-merge-requests-count="openMergeRequestsCount"
+ :latest-version="latestVersion"
+ :web-path="resource.webPath"
+ />
+ <div
+ v-if="isLoadingSharedData"
+ class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-my-3 gl-max-w-20!"
+ ></div>
+ <p v-else class="gl-mt-3">
+ {{ resource.description }}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue
new file mode 100644
index 00000000000..83ea224d772
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue
@@ -0,0 +1,13 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div class="gl-display-flex">
+ <div class="gl-animate-skeleton-loader gl-h-11 gl-rounded-base gl-w-11"></div>
+ <div class="gl-pl-4 gl--flex-center gl-flex-direction-column">
+ <div class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-mb-3 gl-w-20"></div>
+ <div class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-w-20"></div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
new file mode 100644
index 00000000000..d473833869d
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import getCiCatalogResourceReadme from '../../graphql/queries/get_ci_catalog_resource_readme.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ directives: { SafeHtml },
+ props: {
+ resourceId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ readmeHtml: null,
+ };
+ },
+ apollo: {
+ readmeHtml: {
+ query: getCiCatalogResourceReadme,
+ variables() {
+ return {
+ id: this.resourceId,
+ };
+ },
+ update(data) {
+ return data?.ciCatalogResource?.readmeHtml || null;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.loadingError });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.readmeHtml.loading;
+ },
+ },
+ i18n: {
+ loadingError: __("There was a problem loading this project's readme content."),
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
+ <div v-else v-safe-html="readmeHtml"></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
new file mode 100644
index 00000000000..487215875c0
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlBanner, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
+
+export default {
+ components: {
+ GlBanner,
+ GlLink,
+ },
+ inject: ['pageTitle', 'pageDescription'],
+ data() {
+ return {
+ isFeedbackBannerDismissed: localStorage.getItem(CATALOG_FEEDBACK_DISMISSED_KEY) === 'true',
+ };
+ },
+ methods: {
+ handleDismissBanner() {
+ localStorage.setItem(CATALOG_FEEDBACK_DISMISSED_KEY, 'true');
+ this.isFeedbackBannerDismissed = true;
+ },
+ },
+ i18n: {
+ banner: {
+ title: __('Your feedback is important to us 👋'),
+ description: s__(
+ "CiCatalog|We want to help you create and manage pipeline component repositories, while also making it easier to reuse pipeline configurations. Let us know how we're doing!",
+ ),
+ btnText: __('Give us some feedback'),
+ },
+ learnMore: __('Learn more'),
+ },
+ learnMorePath: helpPagePath('ci/components/index'),
+};
+</script>
+<template>
+ <div class="gl-border-b-1 gl-border-gray-100 gl-border-b-solid">
+ <gl-banner
+ v-if="!isFeedbackBannerDismissed"
+ class="gl-mt-5"
+ :title="$options.i18n.banner.title"
+ :button-text="$options.i18n.banner.btnText"
+ button-link="https://gitlab.com/gitlab-org/gitlab/-/issues/407556"
+ @close="handleDismissBanner"
+ >
+ <p>
+ {{ $options.i18n.banner.description }}
+ </p>
+ </gl-banner>
+ <h1 class="gl-font-size-h-display">{{ pageTitle }}</h1>
+ <p>
+ <span>{{ pageDescription }}</span>
+ <gl-link :href="$options.learnMorePath" target="_blank">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
new file mode 100644
index 00000000000..3722b8e6c59
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+ data() {
+ return {
+ coordinates: {
+ statsX: 0,
+ releaseDateX: 0,
+ },
+ width: 0,
+ };
+ },
+ mounted() {
+ this.setSvgSize();
+ },
+ methods: {
+ setSvgSize() {
+ this.width = this.$el.offsetWidth;
+ this.coordinates.releaseDateX = this.width - 200;
+ this.coordinates.statsX = this.width - 90;
+ },
+ },
+ skeletonLoadItems: new Array(5),
+};
+</script>
+<template>
+ <div class="gl-w-full">
+ <gl-skeleton-loader
+ v-for="(_, index) in $options.skeletonLoadItems"
+ :key="index"
+ :height="60"
+ :width="width"
+ >
+ <!-- Catalog project avatar -->
+ <rect x="0" y="0" width="48" height="48" rx="4" ry="4" />
+ <!-- namespace path -->
+ <rect x="60" y="4" width="400" height="16" rx="2" ry="2" />
+ <!-- Project description -->
+ <rect x="60" y="30" width="500" height="12" rx="2" ry="2" />
+
+ <!-- Release date line -->
+ <rect :x="coordinates.releaseDateX" y="30" width="200" height="12" rx="2" ry="2" />
+
+ <!-- Favorites -->
+ <rect :x="coordinates.statsX" y="4" width="16" height="16" rx="2" ry="2" />
+ <rect :x="coordinates.statsX + 18" y="7" width="18" height="10" rx="2" ry="2" />
+
+ <!-- Forks -->
+ <rect :x="coordinates.statsX + 50" y="4" width="16" height="16" rx="2" ry="2" />
+ <rect :x="coordinates.statsX + 68" y="7" width="18" height="10" rx="2" ry="2" />
+ </gl-skeleton-loader>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue
new file mode 100644
index 00000000000..d1fd9fe977b
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+
+import { s__, sprintf } from '~/locale';
+import { ciCatalogResourcesItemsCount } from '../../graphql/settings';
+import CiResourcesListItem from './ci_resources_list_item.vue';
+
+export default {
+ components: {
+ CiResourcesListItem,
+ GlKeysetPagination,
+ },
+ props: {
+ currentPage: {
+ type: Number,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ resources: {
+ type: Array,
+ required: true,
+ },
+ totalCount: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ showPageCount() {
+ return typeof this.totalPageCount === 'number' && this.totalPageCount > 0;
+ },
+ totalPageCount() {
+ return Math.ceil(this.totalCount / ciCatalogResourcesItemsCount);
+ },
+ pageText() {
+ return sprintf(this.$options.i18n.pageText, {
+ currentPage: this.currentPage,
+ totalPage: this.totalPageCount,
+ });
+ },
+ },
+ i18n: {
+ pageText: s__('CiCatalog|Page %{currentPage} of %{totalPage}'),
+ },
+};
+</script>
+<template>
+ <div>
+ <ul class="gl-p-0" data-testId="catalog-list-container">
+ <ci-resources-list-item
+ v-for="resource in resources"
+ :key="resource.id"
+ :resource="resource"
+ />
+ </ul>
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-bind="pageInfo"
+ @prev="$emit('onPrevPage')"
+ @next="$emit('onNextPage')"
+ />
+ </div>
+ <div
+ v-if="showPageCount"
+ class="gl-display-flex gl-justify-content-center gl-mt-3"
+ data-testid="pageCount"
+ >
+ {{ pageText }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
new file mode 100644
index 00000000000..63243539575
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
@@ -0,0 +1,144 @@
+<script>
+import {
+ GlAvatar,
+ GlBadge,
+ GlButton,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
+import { CI_RESOURCE_DETAILS_PAGE_NAME } from '../../router/constants';
+
+export default {
+ i18n: {
+ unreleased: s__('CiCatalog|Unreleased'),
+ releasedMessage: s__('CiCatalog|Released %{timeAgo} by %{author}'),
+ },
+ components: {
+ GlAvatar,
+ GlBadge,
+ GlButton,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ resource: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ authorName() {
+ return this.latestVersion.author.name;
+ },
+ authorProfileUrl() {
+ return this.latestVersion.author.webUrl;
+ },
+ entityId() {
+ return getIdFromGraphQLId(this.resource.id);
+ },
+ starCount() {
+ return this.resource?.starCount || 0;
+ },
+ forksCount() {
+ return this.resource?.forksCount || 0;
+ },
+ hasReleasedVersion() {
+ return Boolean(this.latestVersion?.releasedAt);
+ },
+ formattedDate() {
+ return formatDate(this.latestVersion?.releasedAt);
+ },
+ latestVersion() {
+ return this.resource?.latestVersion || {};
+ },
+ releasedAt() {
+ return getTimeago().format(this.latestVersion?.releasedAt);
+ },
+ resourcePath() {
+ return `${this.resource.rootNamespace?.name} / ${this.resource.rootNamespace?.fullPath} / `;
+ },
+ tagName() {
+ return this.latestVersion?.tagName || this.$options.i18n.unreleased;
+ },
+ },
+ methods: {
+ navigateToDetailsPage() {
+ this.$router.push({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: { id: this.entityId },
+ });
+ },
+ },
+};
+</script>
+<template>
+ <li
+ class="gl-display-flex gl-display-flex-wrap gl-border-b-1 gl-border-gray-100 gl-border-b-solid gl-text-gray-500 gl-py-3"
+ data-testid="catalog-resource-item"
+ >
+ <gl-avatar
+ class="gl-mr-4"
+ :entity-id="entityId"
+ :entity-name="resource.name"
+ shape="rect"
+ :size="48"
+ :src="resource.icon"
+ @click="navigateToDetailsPage"
+ />
+ <div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
+ <div class="gl-display-flex gl-flex-wrap gl-gap-2 gl-mb-2">
+ <gl-button
+ variant="link"
+ class="gl-text-gray-900! gl-mr-1"
+ data-testid="ci-resource-link"
+ @click="navigateToDetailsPage"
+ >
+ {{ resourcePath }} <b> {{ resource.name }}</b>
+ </gl-button>
+ <div class="gl-display-flex gl-flex-grow-1 gl-md-justify-content-space-between">
+ <gl-badge size="sm">{{ tagName }}</gl-badge>
+ <span class="gl-display-flex gl-align-items-center gl-ml-5">
+ <span class="gl--flex-center" data-testid="stats-favorites">
+ <gl-icon name="star" :size="14" class="gl-mr-1" />
+ <span class="gl-mr-3">{{ starCount }}</span>
+ </span>
+ <span class="gl--flex-center" data-testid="stats-forks">
+ <gl-icon name="fork" :size="14" class="gl-mr-1" />
+ <span>{{ forksCount }}</span>
+ </span>
+ </span>
+ </div>
+ </div>
+ <div class="gl-display-flex gl-sm-flex-direction-column gl-justify-content-space-between">
+ <span class="gl-display-flex gl-flex-basis-two-thirds gl-font-sm">{{
+ resource.description
+ }}</span>
+ <div class="gl-display-flex gl-justify-content-end">
+ <span v-if="hasReleasedVersion">
+ <gl-sprintf :message="$options.i18n.releasedMessage">
+ <template #timeAgo>
+ <span v-gl-tooltip.bottom :title="formattedDate">
+ {{ releasedAt }}
+ </span>
+ </template>
+ <template #author>
+ <gl-link :href="authorProfileUrl" data-testid="user-link">
+ <span>{{ authorName }}</span>
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/empty_state.vue b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
new file mode 100644
index 00000000000..a53ddefaa50
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
@@ -0,0 +1,22 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('CiCatalog|Get started with the CI/CD Catalog'),
+ description: s__(
+ 'CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier.',
+ ),
+ },
+ name: 'CiCatalogEmptyState',
+ components: {
+ GlEmptyState,
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-empty-state :title="$options.i18n.title" :description="$options.i18n.description" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
new file mode 100644
index 00000000000..da2c73be900
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { CI_CATALOG_RESOURCE_TYPE } from '../../graphql/settings';
+import getCatalogCiResourceDetails from '../../graphql/queries/get_ci_catalog_resource_details.query.graphql';
+import getCatalogCiResourceSharedData from '../../graphql/queries/get_ci_catalog_resource_shared_data.query.graphql';
+import CiResourceDetails from '../details/ci_resource_details.vue';
+import CiResourceHeader from '../details/ci_resource_header.vue';
+
+export default {
+ components: {
+ CiResourceDetails,
+ CiResourceHeader,
+ GlEmptyState,
+ },
+ inject: ['ciCatalogPath'],
+ data() {
+ return {
+ isEmpty: false,
+ resourceSharedData: {},
+ resourceAdditionalDetails: {},
+ };
+ },
+ apollo: {
+ resourceSharedData: {
+ query: getCatalogCiResourceSharedData,
+ variables() {
+ return {
+ id: this.graphQLId,
+ };
+ },
+ update(data) {
+ return data.ciCatalogResource;
+ },
+ error(e) {
+ this.isEmpty = true;
+ createAlert({ message: e.message });
+ },
+ },
+ resourceAdditionalDetails: {
+ query: getCatalogCiResourceDetails,
+ variables() {
+ return {
+ id: this.graphQLId,
+ };
+ },
+ update(data) {
+ return data.ciCatalogResource;
+ },
+ error(e) {
+ this.isEmpty = true;
+ createAlert({ message: e.message });
+ },
+ },
+ },
+ computed: {
+ graphQLId() {
+ return convertToGraphQLId(CI_CATALOG_RESOURCE_TYPE, this.$route.params.id);
+ },
+ isLoadingDetails() {
+ return this.$apollo.queries.resourceAdditionalDetails.loading;
+ },
+ isLoadingSharedData() {
+ return this.$apollo.queries.resourceSharedData.loading;
+ },
+ versions() {
+ return this.resourceAdditionalDetails?.versions?.nodes || [];
+ },
+ pipelineStatus() {
+ return (
+ this.resourceAdditionalDetails?.versions?.nodes[0]?.commit?.pipelines?.nodes[0]
+ ?.detailedStatus || null
+ );
+ },
+ },
+ i18n: {
+ emptyStateTitle: s__('CiCatalog|No component available'),
+ emptyStateDescription: s__(
+ 'CiCatalog|Component ID not found, or you do not have permission to access component.',
+ ),
+ emptyStateButtonText: s__('CiCatalog|Back to the CI/CD Catalog'),
+ },
+};
+</script>
+<template>
+ <div>
+ <div v-if="isEmpty" class="gl-display-flex">
+ <gl-empty-state
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ :primary-button-text="$options.i18n.emptyStateButtonText"
+ :primary-button-link="ciCatalogPath"
+ />
+ </div>
+ <div v-else>
+ <ci-resource-header
+ :open-issues-count="resourceAdditionalDetails.openIssuesCount"
+ :open-merge-requests-count="resourceAdditionalDetails.openMergeRequestsCount"
+ :is-loading-details="isLoadingDetails"
+ :is-loading-shared-data="isLoadingSharedData"
+ :pipeline-status="pipelineStatus"
+ :resource="resourceSharedData"
+ />
+ <ci-resource-details :resource-id="graphQLId" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/constants.js b/app/assets/javascripts/ci/catalog/constants.js
new file mode 100644
index 00000000000..ab067f991cd
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/constants.js
@@ -0,0 +1,35 @@
+// We disable this for the entire file until the mock data is cleanup
+/* eslint-disable @gitlab/require-i18n-strings */
+export const CATALOG_FEEDBACK_DISMISSED_KEY = 'catalog_feedback_dismissed';
+
+export const componentsMockData = {
+ __typename: 'CiComponentConnection',
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Component/1',
+ name: 'Ruby gal',
+ description: 'This is a pretty amazing component that does EVERYTHING ruby.',
+ path: 'gitlab.com/gitlab-org/ruby-gal@~latest',
+ inputs: { nodes: [{ name: 'version', defaultValue: '1.0.0', required: true }] },
+ },
+ {
+ id: 'gid://gitlab/Ci::Component/2',
+ name: 'Javascript madness',
+ description: 'Adds some spice to your life.',
+ path: 'gitlab.com/gitlab-org/javascript-madness@~latest',
+ inputs: {
+ nodes: [
+ { name: 'isFun', defaultValue: 'true', required: true },
+ { name: 'RandomNumber', defaultValue: '10', required: false },
+ ],
+ },
+ },
+ {
+ id: 'gid://gitlab/Ci::Component/3',
+ name: 'Go go go',
+ description: 'When you write Go, you gotta go go go.',
+ path: 'gitlab.com/gitlab-org/go-go-go@~latest',
+ inputs: { nodes: [{ name: 'version', defaultValue: '1.0.0', required: true }] },
+ },
+ ],
+};
diff --git a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
new file mode 100644
index 00000000000..f4d1bb0eaaf
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
@@ -0,0 +1,25 @@
+fragment CatalogResourceFields on CiCatalogResource {
+ id
+ icon
+ name
+ description
+ starCount
+ forksCount
+ latestVersion {
+ id
+ tagName
+ tagPath
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ rootNamespace {
+ id
+ fullPath
+ name
+ }
+ webPath
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
new file mode 100644
index 00000000000..6aef5dcc4e7
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
@@ -0,0 +1,20 @@
+query getCiCatalogResourceComponents($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ id
+ components @client {
+ nodes {
+ id
+ name
+ description
+ path
+ inputs {
+ nodes {
+ name
+ defaultValue
+ required
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
new file mode 100644
index 00000000000..382d3866795
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
@@ -0,0 +1,29 @@
+query getCiCatalogResourceDetails($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ id
+ openIssuesCount
+ openMergeRequestsCount
+ versions(first: 1) {
+ nodes {
+ id
+ commit {
+ id
+ pipelines(first: 1) {
+ nodes {
+ id
+ detailedStatus {
+ id
+ detailsPath
+ icon
+ text
+ group
+ }
+ }
+ }
+ }
+ tagName
+ releasedAt
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
new file mode 100644
index 00000000000..6b3d0cdcfc7
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
@@ -0,0 +1,6 @@
+query getCiCatalogResourceReadme($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ id
+ readmeHtml
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
new file mode 100644
index 00000000000..4ac4cb0e394
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
@@ -0,0 +1,7 @@
+#import "../fragments/catalog_resource.fragment.graphql"
+
+query getCiCatalogResourceSharedData($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ ...CatalogResourceFields
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/settings.js b/app/assets/javascripts/ci/catalog/graphql/settings.js
new file mode 100644
index 00000000000..a87b26ca4fc
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/settings.js
@@ -0,0 +1,32 @@
+import { componentsMockData } from '../constants';
+
+export const ciCatalogResourcesItemsCount = 20;
+export const CI_CATALOG_RESOURCE_TYPE = 'Ci::Catalog::Resource';
+
+export const cacheConfig = {
+ cacheConfig: {
+ typePolicies: {
+ Query: {
+ fields: {
+ ciCatalogResource(_, { args, toReference }) {
+ return toReference({
+ __typename: 'CiCatalogResource',
+ id: args.id,
+ });
+ },
+ ciCatalogResources: {
+ keyArgs: false,
+ },
+ },
+ },
+ },
+ },
+};
+
+export const resolvers = {
+ CiCatalogResource: {
+ components() {
+ return componentsMockData;
+ },
+ },
+};
diff --git a/app/assets/javascripts/ci/catalog/router/constants.js b/app/assets/javascripts/ci/catalog/router/constants.js
new file mode 100644
index 00000000000..2d9462ef403
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/router/constants.js
@@ -0,0 +1,2 @@
+export const CI_RESOURCES_PAGE_NAME = 'ci_resources';
+export const CI_RESOURCE_DETAILS_PAGE_NAME = 'ci_resources_details';
diff --git a/app/assets/javascripts/ci/catalog/router/index.js b/app/assets/javascripts/ci/catalog/router/index.js
new file mode 100644
index 00000000000..0b2b3dd3aa3
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/router/index.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { createRoutes } from './routes';
+
+Vue.use(VueRouter);
+
+export const createRouter = (base, listComponent) => {
+ return new VueRouter({
+ base,
+ mode: 'history',
+ routes: createRoutes(listComponent),
+ });
+};
diff --git a/app/assets/javascripts/ci/catalog/router/routes.js b/app/assets/javascripts/ci/catalog/router/routes.js
new file mode 100644
index 00000000000..ccfb0673c83
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/router/routes.js
@@ -0,0 +1,9 @@
+import CiResourceDetailsPage from '../components/pages/ci_resource_details_page.vue';
+import { CI_RESOURCES_PAGE_NAME, CI_RESOURCE_DETAILS_PAGE_NAME } from './constants';
+
+export const createRoutes = (listComponent) => {
+ return [
+ { name: CI_RESOURCES_PAGE_NAME, path: '', component: listComponent },
+ { name: CI_RESOURCE_DETAILS_PAGE_NAME, path: '/:id', component: CiResourceDetailsPage },
+ ];
+};
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
index a25f871ac92..77af643cbb3 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
@@ -24,10 +24,6 @@ export default {
type: Array,
required: true,
},
- hasEnvScopeQuery: {
- type: Boolean,
- required: true,
- },
selectedEnvironmentScope: {
type: String,
required: false,
@@ -36,6 +32,7 @@ export default {
},
data() {
return {
+ customEnvScope: null,
isDropdownShown: false,
selectedEnvironment: '',
searchTerm: '',
@@ -45,42 +42,38 @@ export default {
composedCreateButtonLabel() {
return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
},
- filteredEnvironments() {
- const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.environments.filter((environment) => {
- return environment.toLowerCase().includes(lowerCasedSearchTerm);
- });
- },
isDropdownLoading() {
- return this.areEnvironmentsLoading && this.hasEnvScopeQuery && !this.isDropdownShown;
+ return this.areEnvironmentsLoading && !this.isDropdownShown;
},
isDropdownSearching() {
- return this.areEnvironmentsLoading && this.hasEnvScopeQuery && this.isDropdownShown;
+ return this.areEnvironmentsLoading && this.isDropdownShown;
},
searchedEnvironments() {
- // If hasEnvScopeQuery (applies only to projects for now), search query will be fired so this
- // component will already receive filtered environments during the refetch.
- // Otherwise (applies to groups), search the existing list of environments in the frontend
- let filtered = this.hasEnvScopeQuery ? this.environments : this.filteredEnvironments;
+ let filtered = this.environments;
// If there is no search term, make sure to include *
- if (this.hasEnvScopeQuery && !this.searchTerm) {
+ if (!this.searchTerm) {
filtered = uniq([...filtered, '*']);
}
+ // add custom env scope if it matches the search term
+ if (this.customEnvScope && this.customEnvScope.startsWith(this.searchTerm)) {
+ filtered = uniq([...filtered, this.customEnvScope]);
+ }
+
return filtered.sort().map((environment) => ({
value: environment,
text: environment,
}));
},
shouldRenderCreateButton() {
- return this.searchTerm && !this.environments.includes(this.searchTerm);
- },
- shouldRenderDivider() {
return (
- (this.hasEnvScopeQuery || this.shouldRenderCreateButton) && !this.areEnvironmentsLoading
+ this.searchTerm && ![...this.environments, this.customEnvScope].includes(this.searchTerm)
);
},
+ shouldRenderDivider() {
+ return !this.areEnvironmentsLoading;
+ },
environmentScopeLabel() {
return convertEnvironmentScope(this.selectedEnvironmentScope);
},
@@ -89,16 +82,14 @@ export default {
debouncedSearch: debounce(function debouncedSearch(searchTerm) {
const newSearchTerm = searchTerm.trim();
this.searchTerm = newSearchTerm;
- if (this.hasEnvScopeQuery) {
- this.$emit('search-environment-scope', newSearchTerm);
- }
+ this.$emit('search-environment-scope', newSearchTerm);
}, 500),
selectEnvironment(selected) {
this.$emit('select-environment', selected);
this.selectedEnvironment = selected;
},
createEnvironmentScope() {
- this.$emit('create-environment-scope', this.searchTerm);
+ this.customEnvScope = this.searchTerm;
this.selectEnvironment(this.searchTerm);
},
toggleDropdownShown(isShown) {
@@ -129,7 +120,7 @@ export default {
>
<template #footer>
<gl-dropdown-divider v-if="shouldRenderDivider" />
- <div v-if="hasEnvScopeQuery" data-testid="max-envs-notice">
+ <div data-testid="max-envs-notice">
<gl-dropdown-item class="gl-list-style-none" disabled>
<gl-sprintf :message="$options.i18n.maxEnvsNote" class="gl-font-sm">
<template #limit>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
index c609e05bbb7..a32c5f476fb 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
@@ -11,9 +11,11 @@ import {
GlFormTextarea,
GlIcon,
GlLink,
+ GlModal,
+ GlModalDirective,
GlSprintf,
} from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -36,10 +38,11 @@ import { awsTokenList } from './ci_variable_autocomplete_tokens';
const trackingMixin = Tracking.mixin({ label: DRAWER_EVENT_LABEL });
export const i18n = {
- addVariable: s__('CiVariables|Add Variable'),
+ addVariable: s__('CiVariables|Add variable'),
cancel: __('Cancel'),
defaultScope: allEnvironments.text,
- editVariable: s__('CiVariables|Edit Variable'),
+ deleteVariable: s__('CiVariables|Delete variable'),
+ editVariable: s__('CiVariables|Edit variable'),
environments: __('Environments'),
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
expandedField: s__('CiVariables|Expand variable reference'),
@@ -51,6 +54,7 @@ export const i18n = {
maskedDescription: s__(
'CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements.',
),
+ modalDeleteMessage: s__('CiVariables|Do you want to delete the variable %{key}?'),
protectedField: s__('CiVariables|Protect variable'),
protectedDescription: s__(
'CiVariables|Export variable to pipelines running on protected branches and tags only.',
@@ -86,8 +90,12 @@ export default {
GlFormTextarea,
GlIcon,
GlLink,
+ GlModal,
GlSprintf,
},
+ directives: {
+ GlModalDirective,
+ },
mixins: [trackingMixin],
inject: ['environmentScopeLink', 'isProtectedByDefault', 'maskableRawRegex', 'maskableRegex'],
props: {
@@ -170,6 +178,9 @@ export default {
modalActionText() {
return this.isEditing ? this.$options.i18n.editVariable : this.$options.i18n.addVariable;
},
+ removeVariableMessage() {
+ return sprintf(this.$options.i18n.modalDeleteMessage, { key: this.variable.key });
+ },
},
watch: {
variable: {
@@ -188,6 +199,13 @@ export default {
close() {
this.$emit('close-form');
},
+ deleteVariable() {
+ this.$emit('delete-variable', this.variable);
+ this.close();
+ },
+ setEnvironmentScope(scope) {
+ this.variable = { ...this.variable, environmentScope: scope };
+ },
getTrackingErrorProperty() {
if (this.isValueEmpty) {
return null;
@@ -225,164 +243,206 @@ export default {
}),
i18n,
variableOptions,
+ deleteModal: {
+ actionPrimary: {
+ text: __('Delete'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionSecondary: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
};
</script>
<template>
- <gl-drawer
- open
- data-testid="ci-variable-drawer"
- :header-height="getDrawerHeaderHeight"
- :z-index="$options.DRAWER_Z_INDEX"
- @close="close"
- >
- <template #title>
- <h2 class="gl-m-0">{{ modalActionText }}</h2>
- </template>
- <gl-form-group
- :label="$options.i18n.type"
- label-for="ci-variable-type"
- class="gl-border-none"
- :class="{
- 'gl-mb-n5': !hideEnvironmentScope,
- 'gl-mb-n1': hideEnvironmentScope,
- }"
+ <div>
+ <gl-drawer
+ open
+ data-testid="ci-variable-drawer"
+ :header-height="getDrawerHeaderHeight"
+ :z-index="$options.DRAWER_Z_INDEX"
+ @close="close"
>
- <gl-form-select
- id="ci-variable-type"
- v-model="variable.variableType"
- :options="$options.variableOptions"
- />
- </gl-form-group>
- <gl-form-group
- v-if="!hideEnvironmentScope"
- class="gl-border-none gl-mb-n5"
- label-for="ci-variable-env"
- data-testid="environment-scope"
- >
- <template #label>
- <div class="gl-display-flex gl-align-items-center">
- <span class="gl-mr-2">
- {{ $options.i18n.environments }}
- </span>
- <gl-link
- class="gl-display-flex"
- :title="$options.i18n.environmentScopeLinkTitle"
- :href="environmentScopeLink"
- target="_blank"
- data-testid="environment-scope-link"
- >
- <gl-icon name="question-o" :size="14" />
- </gl-link>
- </div>
+ <template #title>
+ <h2 class="gl-m-0">{{ modalActionText }}</h2>
</template>
- <ci-environments-dropdown
- v-if="areScopedVariablesAvailable"
- class="gl-mb-5"
- has-env-scope-query
- :are-environments-loading="areEnvironmentsLoading"
- :environments="environments"
- :selected-environment-scope="variable.environmentScope"
- />
- <gl-form-input
- v-else
- :value="$options.i18n.defaultScope"
- class="gl-w-full gl-mb-5"
- readonly
+ <gl-form-group
+ :label="$options.i18n.type"
+ label-for="ci-variable-type"
+ class="gl-border-none"
+ :class="{
+ 'gl-mb-n5': !hideEnvironmentScope,
+ 'gl-mb-n1': hideEnvironmentScope,
+ }"
+ >
+ <gl-form-select
+ id="ci-variable-type"
+ v-model="variable.variableType"
+ :options="$options.variableOptions"
+ />
+ </gl-form-group>
+ <gl-form-group
+ v-if="!hideEnvironmentScope"
+ class="gl-border-none gl-mb-n5"
+ label-for="ci-variable-env"
+ data-testid="environment-scope"
+ >
+ <template #label>
+ <div class="gl-display-flex gl-align-items-center">
+ <span class="gl-mr-2">
+ {{ $options.i18n.environments }}
+ </span>
+ <gl-link
+ class="gl-display-flex"
+ :title="$options.i18n.environmentScopeLinkTitle"
+ :href="environmentScopeLink"
+ target="_blank"
+ data-testid="environment-scope-link"
+ >
+ <gl-icon name="question-o" :size="14" />
+ </gl-link>
+ </div>
+ </template>
+ <ci-environments-dropdown
+ v-if="areScopedVariablesAvailable"
+ class="gl-mb-5"
+ :are-environments-loading="areEnvironmentsLoading"
+ :environments="environments"
+ :selected-environment-scope="variable.environmentScope"
+ @select-environment="setEnvironmentScope"
+ @search-environment-scope="$emit('search-environment-scope', $event)"
+ />
+ <gl-form-input
+ v-else
+ :value="$options.i18n.defaultScope"
+ class="gl-w-full gl-mb-5"
+ readonly
+ />
+ </gl-form-group>
+ <gl-form-group class="gl-border-none gl-mb-n8">
+ <template #label>
+ <div class="gl-display-flex gl-align-items-center gl-mb-n3">
+ <span class="gl-mr-2">
+ {{ $options.i18n.flags }}
+ </span>
+ <gl-link
+ class="gl-display-flex"
+ :title="$options.i18n.flagsLinkTitle"
+ :href="$options.flagLink"
+ target="_blank"
+ >
+ <gl-icon name="question-o" :size="14" />
+ </gl-link>
+ </div>
+ </template>
+ <gl-form-checkbox v-model="variable.protected" data-testid="ci-variable-protected-checkbox">
+ {{ $options.i18n.protectedField }}
+ <p class="gl-text-secondary">
+ {{ $options.i18n.protectedDescription }}
+ </p>
+ </gl-form-checkbox>
+ <gl-form-checkbox v-model="variable.masked" data-testid="ci-variable-masked-checkbox">
+ {{ $options.i18n.maskedField }}
+ <p class="gl-text-secondary">{{ $options.i18n.maskedDescription }}</p>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ data-testid="ci-variable-expanded-checkbox"
+ :checked="isExpanded"
+ @change="setRaw"
+ >
+ {{ $options.i18n.expandedField }}
+ <p class="gl-text-secondary">
+ <gl-sprintf :message="$options.i18n.expandedDescription" class="gl-text-secondary">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-form-checkbox>
+ </gl-form-group>
+ <gl-form-combobox
+ v-model="variable.key"
+ :token-list="$options.awsTokenList"
+ :label-text="$options.i18n.key"
+ class="gl-border-none gl-pb-0! gl-mb-n5"
+ data-testid="ci-variable-key"
+ data-qa-selector="ci_variable_key_field"
/>
- </gl-form-group>
- <gl-form-group class="gl-border-none gl-mb-n8">
- <template #label>
- <div class="gl-display-flex gl-align-items-center gl-mb-n3">
- <span class="gl-mr-2">
- {{ $options.i18n.flags }}
- </span>
- <gl-link
- class="gl-display-flex"
- :title="$options.i18n.flagsLinkTitle"
- :href="$options.flagLink"
- target="_blank"
- >
- <gl-icon name="question-o" :size="14" />
- </gl-link>
- </div>
- </template>
- <gl-form-checkbox v-model="variable.protected" data-testid="ci-variable-protected-checkbox">
- {{ $options.i18n.protectedField }}
- <p class="gl-text-secondary">
- {{ $options.i18n.protectedDescription }}
- </p>
- </gl-form-checkbox>
- <gl-form-checkbox v-model="variable.masked" data-testid="ci-variable-masked-checkbox">
- {{ $options.i18n.maskedField }}
- <p class="gl-text-secondary">{{ $options.i18n.maskedDescription }}</p>
- </gl-form-checkbox>
- <gl-form-checkbox
- data-testid="ci-variable-expanded-checkbox"
- :checked="isExpanded"
- @change="setRaw"
+ <gl-form-group
+ :label="$options.i18n.value"
+ label-for="ci-variable-value"
+ class="gl-border-none gl-mb-n2"
+ data-testid="ci-variable-value-label"
+ :invalid-feedback="maskedReqsNotMetText"
+ :state="isValueValid"
>
- {{ $options.i18n.expandedField }}
- <p class="gl-text-secondary">
- <gl-sprintf :message="$options.i18n.expandedDescription" class="gl-text-secondary">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
+ <gl-form-textarea
+ id="ci-variable-value"
+ v-model="variable.value"
+ class="gl-border-none gl-font-monospace!"
+ rows="3"
+ max-rows="10"
+ data-testid="ci-variable-value"
+ data-qa-selector="ci_variable_value_field"
+ spellcheck="false"
+ />
+ <p
+ v-if="variable.raw"
+ class="gl-mt-2 gl-mb-0 text-secondary"
+ data-testid="raw-variable-tip"
+ >
+ {{ $options.i18n.valueFeedback.rawHelpText }}
</p>
- </gl-form-checkbox>
- </gl-form-group>
- <gl-form-combobox
- v-model="variable.key"
- :token-list="$options.awsTokenList"
- :label-text="$options.i18n.key"
- class="gl-border-none gl-pb-0! gl-mb-n5"
- data-testid="ci-variable-key"
- data-qa-selector="ci_variable_key_field"
- />
- <gl-form-group
- :label="$options.i18n.value"
- label-for="ci-variable-value"
- class="gl-border-none gl-mb-n2"
- data-testid="ci-variable-value-label"
- :invalid-feedback="maskedReqsNotMetText"
- :state="isValueValid"
- >
- <gl-form-textarea
- id="ci-variable-value"
- v-model="variable.value"
- class="gl-border-none gl-font-monospace!"
- rows="3"
- max-rows="10"
- data-testid="ci-variable-value"
- data-qa-selector="ci_variable_value_field"
- spellcheck="false"
- />
- <p v-if="variable.raw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip">
- {{ $options.i18n.valueFeedback.rawHelpText }}
- </p>
- </gl-form-group>
- <gl-alert
- v-if="hasVariableReference"
- :title="$options.i18n.variableReferenceTitle"
- :dismissible="false"
- variant="warning"
- class="gl-mx-4 gl-pl-9! gl-border-bottom-0"
- data-testid="has-variable-reference-alert"
+ </gl-form-group>
+ <gl-alert
+ v-if="hasVariableReference"
+ :title="$options.i18n.variableReferenceTitle"
+ :dismissible="false"
+ variant="warning"
+ class="gl-mx-4 gl-pl-9! gl-border-bottom-0"
+ data-testid="has-variable-reference-alert"
+ >
+ {{ $options.i18n.variableReferenceDescription }}
+ </gl-alert>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
+ >{{ $options.i18n.cancel }}
+ </gl-button>
+ <gl-button
+ v-if="isEditing"
+ v-gl-modal-directive="`delete-variable-${variable.key}`"
+ variant="danger"
+ category="secondary"
+ class="gl-mr-3"
+ data-testid="ci-variable-delete-btn"
+ >{{ $options.i18n.deleteVariable }}</gl-button
+ >
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :disabled="!canSubmit"
+ data-testid="ci-variable-confirm-btn"
+ data-qa-selector="ci_variable_save_button"
+ @click="submit"
+ >{{ modalActionText }}
+ </gl-button>
+ </div>
+ </gl-drawer>
+ <gl-modal
+ ref="modal"
+ :modal-id="`delete-variable-${variable.key}`"
+ :title="$options.i18n.deleteVariable"
+ :action-primary="$options.deleteModal.actionPrimary"
+ :action-secondary="$options.deleteModal.actionSecondary"
+ data-testid="ci-variable-drawer-confirm-delete-modal"
+ @primary="deleteVariable"
>
- {{ $options.i18n.variableReferenceDescription }}
- </gl-alert>
- <div class="gl-display-flex gl-justify-content-end">
- <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
- >{{ $options.i18n.cancel }}
- </gl-button>
- <gl-button
- category="primary"
- variant="confirm"
- :disabled="!canSubmit"
- data-testid="ci-variable-confirm-btn"
- @click="submit"
- >{{ modalActionText }}
- </gl-button>
- </div>
- </gl-drawer>
+ {{ removeVariableMessage }}
+ </gl-modal>
+ </div>
</template>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
index 86c0f34215e..cc664d76267 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
@@ -38,7 +38,6 @@ import {
VARIABLE_ACTIONS,
variableOptions,
} from '../constants';
-import { createJoinedEnvironments } from '../utils';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
@@ -90,10 +89,6 @@ export default {
required: false,
default: false,
},
- hasEnvScopeQuery: {
- type: Boolean,
- required: true,
- },
mode: {
type: String,
required: true,
@@ -147,13 +142,6 @@ export default {
isTipVisible() {
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
},
- environmentsList() {
- if (this.hasEnvScopeQuery) {
- return this.environments;
- }
-
- return createJoinedEnvironments(this.variables, this.environments, this.newEnvironments);
- },
maskedFeedback() {
return this.displayMaskedError
? __('This variable value does not meet the masking requirements.')
@@ -211,9 +199,6 @@ export default {
addVariable() {
this.$emit('add-variable', this.variable);
},
- createEnvironmentScope(env) {
- this.newEnvironments.push(env);
- },
deleteVariable() {
this.$emit('delete-variable', this.variable);
},
@@ -407,11 +392,9 @@ export default {
<ci-environments-dropdown
v-if="areScopedVariablesAvailable"
:are-environments-loading="areEnvironmentsLoading"
- :has-env-scope-query="hasEnvScopeQuery"
:selected-environment-scope="variable.environmentScope"
- :environments="environmentsList"
+ :environments="environments"
@select-environment="setEnvironmentScope"
- @create-environment-scope="createEnvironmentScope"
@search-environment-scope="$emit('search-environment-scope', $event)"
/>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
index 482f6da5617..f2d81b3f271 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
@@ -37,10 +37,6 @@ export default {
required: false,
default: false,
},
- hasEnvScopeQuery: {
- type: Boolean,
- required: true,
- },
isLoading: {
type: Boolean,
required: false,
@@ -125,7 +121,6 @@ export default {
:are-environments-loading="areEnvironmentsLoading"
:are-scoped-variables-available="areScopedVariablesAvailable"
:environments="environments"
- :has-env-scope-query="hasEnvScopeQuery"
:hide-environment-scope="hideEnvironmentScope"
:variables="variables"
:mode="mode"
@@ -144,8 +139,11 @@ export default {
:hide-environment-scope="hideEnvironmentScope"
:selected-variable="selectedVariable"
:mode="mode"
- v-on="$listeners"
+ @add-variable="addVariable"
+ @delete-variable="deleteVariable"
@close-form="closeForm"
+ @update-variable="updateVariable"
+ @search-environment-scope="$emit('search-environment-scope', $event)"
/>
</div>
</div>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
index 3d5ed327dc7..011a424b6c2 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
@@ -2,7 +2,7 @@
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import { mapEnvironmentNames } from '../utils';
import {
ADD_MUTATION_ACTION,
@@ -140,7 +140,7 @@ export default {
this.loadingCounter += 1;
} else {
createAlert({ message: this.$options.tooManyCallsError });
- reportMessageToSentry(this.componentName, this.$options.tooManyCallsError, {});
+ reportToSentry(this.componentName, new Error(this.$options.tooManyCallsError));
}
}
},
@@ -285,7 +285,6 @@ export default {
:are-scoped-variables-available="areScopedVariablesAvailable"
:entity="entity"
:environments="environments"
- :has-env-scope-query="hasEnvScopeQuery"
:hide-environment-scope="hideEnvironmentScope"
:is-loading="isLoading"
:max-variable-limit="maxVariableLimit"
diff --git a/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
index a28ca4eebc9..f243a1cb30b 100644
--- a/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
@@ -1,5 +1,4 @@
fragment BaseCiVariable on CiVariable {
- __typename
id
key
value
diff --git a/app/assets/javascripts/ci/ci_variable_list/utils.js b/app/assets/javascripts/ci/ci_variable_list/utils.js
index 1faa97a5f73..a7e020206ea 100644
--- a/app/assets/javascripts/ci/ci_variable_list/utils.js
+++ b/app/assets/javascripts/ci/ci_variable_list/utils.js
@@ -1,29 +1,6 @@
-import { uniq } from 'lodash';
import { allEnvironments } from './constants';
/**
- * This function takes a list of variable, environments and
- * new environments added through the scope dropdown
- * and create a new Array that concatenate the environment list
- * with the environment scopes find in the variable list. This is
- * useful for variable settings so that we can render a list of all
- * environment scopes available based on the list of envs, the ones the user
- * added explictly and what is found under each variable.
- * @param {Array} variables
- * @param {Array} environments
- * @returns {Array} - Array of environments
- */
-
-export const createJoinedEnvironments = (
- variables = [],
- environments = [],
- newEnvironments = [],
-) => {
- const scopesFromVariables = variables.map((variable) => variable.environmentScope);
- return uniq([...environments, ...newEnvironments, ...scopesFromVariables]).sort();
-};
-
-/**
* This function job is to convert the * wildcard to text when applicable
* in the UI. It uses a constants to compare the incoming value to that
* of the * and then apply the corresponding label if applicable. If there
diff --git a/app/assets/javascripts/ci/common/pipelines_table.vue b/app/assets/javascripts/ci/common/pipelines_table.vue
index 807128d2341..13b5120654a 100644
--- a/app/assets/javascripts/ci/common/pipelines_table.vue
+++ b/app/assets/javascripts/ci/common/pipelines_table.vue
@@ -3,39 +3,52 @@ import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { TRACKING_CATEGORIES } from '~/ci/constants';
+import { PIPELINE_ID_KEY, PIPELINE_IID_KEY, TRACKING_CATEGORIES } from '~/ci/constants';
import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
-import eventHub from '~/ci/event_hub';
import PipelineOperations from '../pipelines_page/components/pipeline_operations.vue';
-import PipelineStopModal from '../pipelines_page/components/pipeline_stop_modal.vue';
import PipelineTriggerer from '../pipelines_page/components/pipeline_triggerer.vue';
import PipelineUrl from '../pipelines_page/components/pipeline_url.vue';
-import PipelinesStatusBadge from '../pipelines_page/components/pipelines_status_badge.vue';
+import PipelineStatusBadge from '../pipelines_page/components/pipeline_status_badge.vue';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
+/**
+ * Pipelines Table
+ *
+ * Presentational component of a table of pipelines. This component does not
+ * fetch the list of pipelines and instead expects it as a prop.
+ * GraphQL actions for pipelines, such as retrying, canceling, etc.
+ * are handled within this component.
+ *
+ * Use this `legacy_pipelines_table_wrapper` if you need a fully functional REST component.
+ *
+ * IMPORTANT: When using this component, make sure to handle the following events:
+ * 1- @refresh-pipeline-table
+ * 2- @cancel-pipeline
+ * 3- @retry-pipeline
+ *
+ */
+
export default {
components: {
GlTableLite,
LegacyPipelineMiniGraph,
PipelineFailedJobsWidget,
PipelineOperations,
- PipelinesStatusBadge,
- PipelineStopModal,
+ PipelineStatusBadge,
PipelineTriggerer,
PipelineUrl,
},
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [Tracking.mixin(), glFeatureFlagMixin()],
+ mixins: [Tracking.mixin()],
inject: {
- withFailedJobsDetails: {
+ useFailedJobsWidget: {
default: false,
},
},
@@ -44,37 +57,21 @@ export default {
type: Array,
required: true,
},
- pipelineScheduleUrl: {
- type: String,
- required: false,
- default: '',
- },
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
- viewType: {
+ pipelineIdType: {
type: String,
- required: true,
- },
- pipelineKeyOption: {
- type: Object,
- required: true,
+ required: false,
+ default: PIPELINE_ID_KEY,
+ validator(value) {
+ return value === PIPELINE_IID_KEY || value === PIPELINE_ID_KEY;
+ },
},
},
- data() {
- return {
- pipelineId: 0,
- pipeline: {},
- endpoint: '',
- cancelingPipeline: null,
- };
- },
computed: {
- showFailedJobsWidget() {
- return this.glFeatures.ciJobFailuresInMr;
- },
tableFields() {
return [
{
@@ -119,10 +116,10 @@ export default {
];
},
tdClasses() {
- return this.withFailedJobsDetails ? 'gl-pb-0! gl-border-none!' : 'pl-p-5!';
+ return this.useFailedJobsWidget ? 'gl-pb-0! gl-border-none!' : 'pl-p-5!';
},
pipelinesWithDetails() {
- if (this.withFailedJobsDetails) {
+ if (this.useFailedJobsWidget) {
return this.pipelines.map((p) => {
return { ...p, _showDetails: true };
});
@@ -131,17 +128,6 @@ export default {
return this.pipelines;
},
},
- watch: {
- pipelines() {
- this.cancelingPipeline = null;
- },
- },
- created() {
- eventHub.$on('openConfirmationModal', this.setModalData);
- },
- beforeDestroy() {
- eventHub.$off('openConfirmationModal', this.setModalData);
- },
methods: {
getDownstreamPipelines(pipeline) {
const downstream = pipeline.triggered;
@@ -151,16 +137,19 @@ export default {
return cleanLeadingSeparator(item.project.full_path);
},
failedJobsCount(pipeline) {
- return pipeline?.failed_builds?.length || 0;
+ // Remove `pipeline?.failed_builds?.length` when we remove `ci_fix_performance_pipelines_json_endpoint`.
+ return pipeline?.failed_builds_count || pipeline?.failed_builds?.length || 0;
},
- setModalData(data) {
- this.pipelineId = data.pipeline.id;
- this.pipeline = data.pipeline;
- this.endpoint = data.endpoint;
+ onRefreshPipelinesTable() {
+ this.$emit('refresh-pipelines-table');
},
- onSubmit() {
- eventHub.$emit('postAction', this.endpoint);
- this.cancelingPipeline = this.pipelineId;
+ onRetryPipeline(pipeline) {
+ // This emit is only used by the `legacy_pipelines_table_wrapper`.
+ this.$emit('retry-pipeline', pipeline);
+ },
+ onCancelPipeline(pipeline) {
+ // This emit is only used by the `legacy_pipelines_table_wrapper`.
+ this.$emit('cancel-pipeline', pipeline);
},
trackPipelineMiniGraph() {
this.track('click_minigraph', { label: TRACKING_CATEGORIES.table });
@@ -168,7 +157,6 @@ export default {
},
TBODY_TR_ATTR: {
'data-testid': 'pipeline-table-row',
- 'data-qa-selector': 'pipeline_row_container',
},
};
</script>
@@ -191,14 +179,13 @@ export default {
</template>
<template #cell(status)="{ item }">
- <pipelines-status-badge :pipeline="item" :view-type="viewType" />
+ <pipeline-status-badge :pipeline="item" />
</template>
<template #cell(pipeline)="{ item }">
<pipeline-url
:pipeline="item"
- :pipeline-schedule-url="pipelineScheduleUrl"
- :pipeline-key="pipelineKeyOption.value"
+ :pipeline-id-type="pipelineIdType"
ref-color="gl-text-black-normal"
/>
</template>
@@ -219,12 +206,17 @@ export default {
</template>
<template #cell(actions)="{ item }">
- <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
+ <pipeline-operations
+ :pipeline="item"
+ @cancel-pipeline="onCancelPipeline"
+ @refresh-pipelines-table="onRefreshPipelinesTable"
+ @retry-pipeline="onRetryPipeline"
+ />
</template>
<template #row-details="{ item }">
<pipeline-failed-jobs-widget
- v-if="showFailedJobsWidget"
+ v-if="useFailedJobsWidget"
:failed-jobs-count="failedJobsCount(item)"
:is-pipeline-active="item.active"
:pipeline-iid="item.iid"
@@ -234,7 +226,5 @@ export default {
/>
</template>
</gl-table-lite>
-
- <pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/common/private/job_action_component.vue b/app/assets/javascripts/ci/common/private/job_action_component.vue
index f649750ce8a..b0fa724d450 100644
--- a/app/assets/javascripts/ci/common/private/job_action_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_action_component.vue
@@ -120,7 +120,7 @@ export default {
:class="cssClass"
:disabled="isDisabled"
class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
- data-testid="ci-action-component"
+ data-testid="ci-action-button"
@click.stop="onClickAction"
>
<div
diff --git a/app/assets/javascripts/ci/constants.js b/app/assets/javascripts/ci/constants.js
index 93c2504dd5d..5b60528f521 100644
--- a/app/assets/javascripts/ci/constants.js
+++ b/app/assets/javascripts/ci/constants.js
@@ -24,19 +24,8 @@ export const SUCCESS_STATUS = 'SUCCESS';
export const PASSED_STATUS = 'passed';
export const MANUAL_STATUS = 'manual';
-// Constants for the ID and IID selection dropdown
-export const PipelineKeyOptions = [
- {
- text: __('Show Pipeline ID'),
- label: __('Pipeline ID'),
- value: 'id',
- },
- {
- text: __('Show Pipeline IID'),
- label: __('Pipeline IID'),
- value: 'iid',
- },
-];
+export const PIPELINE_ID_KEY = 'id';
+export const PIPELINE_IID_KEY = 'iid';
export const RAW_TEXT_WARNING = s__(
'Pipeline|Raw text search is not currently supported. Please use the available search tokens.',
diff --git a/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
index f02d59af1d9..0b079ccb64f 100644
--- a/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
+++ b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
@@ -3,7 +3,7 @@ import { produce } from 'immer';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import getInheritedCiVariables from '../graphql/queries/inherited_ci_variables.query.graphql';
@@ -51,7 +51,7 @@ export default {
this.loadingCounter += 1;
} else {
createAlert({ message: this.$options.i18n.tooManyCallsError });
- reportMessageToSentry(this.$options.name, this.$options.i18n.tooManyCallsError, {});
+ reportToSentry(this.$options.name, new Error(this.$options.i18n.tooManyCallsError));
}
},
error() {
diff --git a/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql b/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql
index b25768632e1..9fac461a47d 100644
--- a/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql
+++ b/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql
@@ -8,7 +8,6 @@ query getInheritedCiVariables($after: String, $first: Int, $fullPath: ID!) {
...PageInfo
}
nodes {
- __typename
id
key
variableType
diff --git a/app/assets/javascripts/ci/job_details/components/job_header.vue b/app/assets/javascripts/ci/job_details/components/job_header.vue
index 13f3eebd447..00d15f87064 100644
--- a/app/assets/javascripts/ci/job_details/components/job_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_header.vue
@@ -89,18 +89,36 @@ export default {
<template>
<header
- class="page-content-header gl-md-display-flex gl-min-h-7"
+ class="page-content-header gl-md-display-flex gl-flex-wrap gl-min-h-7 gl-pb-2! gl-w-full"
data-testid="job-header-content"
>
- <section class="header-main-content gl-mr-3">
- <ci-badge-link class="gl-mr-3" :status="status" />
+ <div
+ v-if="name"
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full"
+ >
+ <h1 class="gl-font-size-h-display gl-my-0 gl-display-inline-block" data-testid="job-name">
+ {{ name }}
+ </h1>
- <strong data-testid="job-name">{{ name }}</strong>
+ <div class="gl-display-flex gl-align-self-start gl-mt-n2">
+ <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
+ <gl-button
+ :aria-label="__('Toggle sidebar')"
+ category="secondary"
+ class="gl-lg-display-none gl-ml-2"
+ icon="chevron-double-lg-left"
+ @click="onClickSidebarButton"
+ />
+ </div>
+ </div>
+ </div>
+ <section class="header-main-content gl-display-flex gl-align-items-center gl-mr-3">
+ <ci-badge-link class="gl-mr-3" :status="status" />
- <template v-if="shouldRenderTriggeredLabel">{{ __('started') }}</template>
- <template v-else>{{ __('created') }}</template>
+ <template v-if="shouldRenderTriggeredLabel">{{ __('Started') }}</template>
+ <template v-else>{{ __('Created') }}</template>
- <timeago-tooltip :time="time" />
+ <timeago-tooltip :time="time" class="gl-mx-2" />
{{ __('by') }}
@@ -133,16 +151,5 @@ export default {
</gl-avatar-link>
</template>
</section>
-
- <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
- <section v-if="$slots.default" data-testid="job-header-action-buttons" class="gl-display-flex">
- <slot></slot>
- </section>
- <gl-button
- class="gl-md-display-none gl-ml-auto gl-align-self-start js-sidebar-build-toggle"
- icon="chevron-double-lg-left"
- :aria-label="__('Toggle sidebar')"
- @click="onClickSidebarButton"
- />
</header>
</template>
diff --git a/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
index 419efcba46d..4a30878bec5 100644
--- a/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
@@ -146,7 +146,7 @@ export default {
// BE returns zero based index, we need to add one to match the line numbers in the DOM
const firstSearchResult = `#L${this.searchResults[0].lineNumber + 1}`;
- const logLine = document.querySelector(`.log-line ${firstSearchResult}`);
+ const logLine = document.querySelector(`.js-log-line ${firstSearchResult}`);
if (logLine) {
setTimeout(() => scrollToElement(logLine));
diff --git a/app/assets/javascripts/ci/job_details/components/log/line.vue b/app/assets/javascripts/ci/job_details/components/log/line.vue
index fa4a12b3dd3..416f75372f9 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line.vue
@@ -56,7 +56,7 @@ export default {
if (window.location.hash) {
const hash = getLocationHash();
- const lineToMatch = `L${line.lineNumber + 1}`;
+ const lineToMatch = `L${line.lineNumber}`;
if (hash === lineToMatch) {
applyHashHighlight = true;
@@ -66,7 +66,11 @@ export default {
return h(
'div',
{
- class: ['js-line', 'log-line', { 'gl-bg-gray-700': isHighlighted || applyHashHighlight }],
+ class: [
+ 'js-log-line',
+ 'log-line',
+ { 'gl-bg-gray-700': isHighlighted || applyHashHighlight },
+ ],
},
[
h(LineNumber, {
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_header.vue b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
index e647ab4ac0b..658a94e6af4 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
@@ -46,7 +46,7 @@ export default {
},
mounted() {
const hash = getLocationHash();
- const lineToMatch = `L${this.line.lineNumber + 1}`;
+ const lineToMatch = `L${this.line.lineNumber}`;
if (hash === lineToMatch) {
this.applyHashHighlight = true;
@@ -62,7 +62,7 @@ export default {
<template>
<div
- class="log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative"
+ class="js-log-line log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative"
:class="{ 'gl-bg-gray-700': isHighlighted || applyHashHighlight }"
role="button"
@click="handleOnClick"
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_number.vue b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
index 7ca9154d2fe..30b4c80f3fa 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_number.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
@@ -14,8 +14,7 @@ export default {
render(h, { props }) {
const { lineNumber, path } = props;
- const parsedLineNumber = lineNumber + 1;
- const lineId = `L${parsedLineNumber}`;
+ const lineId = `L${lineNumber}`;
const lineHref = `${path}#${lineId}`;
return h(
@@ -27,7 +26,7 @@ export default {
href: lineHref,
},
},
- parsedLineNumber,
+ lineNumber,
);
},
};
diff --git a/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
index 1232ffffb57..7f419a249cf 100644
--- a/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
+++ b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
@@ -18,7 +18,7 @@ import { JOB_GRAPHQL_ERRORS } from '~/ci/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { s__ } from '~/locale';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import GetJob from '../graphql/queries/get_job.query.graphql';
import playJobWithVariablesMutation from '../graphql/mutations/job_play_with_variables.mutation.graphql';
import retryJobWithVariablesMutation from '../graphql/mutations/job_retry_with_variables.mutation.graphql';
@@ -57,7 +57,7 @@ export default {
},
error(error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
- reportMessageToSentry(this.$options.name, error, {});
+ reportToSentry(this.$options.name, error);
},
},
},
@@ -141,7 +141,7 @@ export default {
}
} catch (error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText });
- reportMessageToSentry(this.$options.name, error, {});
+ reportToSentry(this.$options.name, error);
}
},
async retryJob() {
@@ -157,7 +157,7 @@ export default {
}
} catch (error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText });
- reportMessageToSentry(this.$options.name, error, {});
+ reportToSentry(this.$options.name, error);
}
},
addEmptyVariable() {
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
index 4c81a9bd033..f6d39e8e4ac 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
@@ -78,7 +78,7 @@ export default {
<span v-if="willExpire" data-testid="artifacts-unlocked-message-content">
{{ $options.i18n.willExpireText }}
</span>
- <timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" />
+ <timeago-tooltip v-if="artifact.expireAt" :time="artifact.expireAt" />
<gl-link
:href="helpUrl"
target="_blank"
@@ -95,23 +95,23 @@ export default {
</p>
<gl-button-group class="gl-display-flex gl-mt-3">
<gl-button
- v-if="artifact.keep_path"
- :href="artifact.keep_path"
+ v-if="artifact.keepPath"
+ :href="artifact.keepPath"
data-method="post"
data-testid="keep-artifacts"
>{{ $options.i18n.keepText }}</gl-button
>
<gl-button
- v-if="artifact.download_path"
- :href="artifact.download_path"
+ v-if="artifact.downloadPath"
+ :href="artifact.downloadPath"
rel="nofollow"
data-testid="download-artifacts"
download
>{{ $options.i18n.downloadText }}</gl-button
>
<gl-button
- v-if="artifact.browse_path"
- :href="artifact.browse_path"
+ v-if="artifact.browsePath"
+ :href="artifact.browsePath"
data-testid="browse-artifacts-button"
>{{ $options.i18n.browseText }}</gl-button
>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
index 95616a4c706..5e826efbefb 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
@@ -25,11 +25,7 @@ export default {
<p class="gl-display-flex gl-flex-wrap gl-align-items-baseline gl-gap-2 gl-mb-0">
<span class="gl-display-flex gl-font-weight-bold">{{ __('Commit') }}</span>
- <gl-link
- :href="commit.commit_path"
- class="gl-text-blue-500! gl-font-monospace"
- data-testid="commit-sha"
- >
+ <gl-link :href="commit.commit_path" class="commit-sha-container" data-testid="commit-sha">
{{ commit.short_id }}
</gl-link>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
index 7f2f4fc0331..231f45d7ae6 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
@@ -4,6 +4,8 @@ import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { forwardDeploymentFailureModalId } from '~/ci/constants';
import { filterAnnotations } from '~/ci/job_details/utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_block.vue';
import ExternalLinksBlock from './external_links_block.vue';
@@ -15,6 +17,9 @@ import StagesDropdown from './stages_dropdown.vue';
import TriggerBlock from './trigger_block.vue';
export default {
+ i18n: {
+ toggleSidebar: __('Toggle Sidebar'),
+ },
name: 'JobSidebar',
forwardDeploymentFailureModalId,
components: {
@@ -42,6 +47,9 @@ export default {
// the artifact object will always have a locked property
return Object.keys(this.job.artifact).length > 1;
},
+ artifact() {
+ return convertObjectPropsToCamelCase(this.job.artifact, { deep: true });
+ },
hasExternalLinks() {
return this.externalLinks.length > 0;
},
@@ -79,36 +87,44 @@ export default {
<template>
<aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
<div class="sidebar-container">
- <div class="blocks-container gl-p-4">
+ <div class="blocks-container gl-p-4 gl-pt-0">
<sidebar-header
- class="block gl-pb-4! gl-mb-2"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
:rest-job="job"
:job-id="job.id"
@updateVariables="$emit('updateVariables')"
/>
- <job-sidebar-details-container class="block gl-mb-2" />
+ <job-sidebar-details-container class="gl-py-4 gl-border-b gl-border-gray-50" />
<artifacts-block
v-if="hasArtifact"
- class="block gl-mb-2"
- :artifact="job.artifact"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
+ :artifact="artifact"
:help-url="artifactHelpUrl"
/>
<external-links-block
v-if="hasExternalLinks"
- class="block gl-mb-2"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
:external-links="externalLinks"
/>
- <trigger-block v-if="hasTriggers" class="block gl-mb-2" :trigger="job.trigger" />
+ <trigger-block
+ v-if="hasTriggers"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
+ :trigger="job.trigger"
+ />
- <commit-block class="block gl-mb-2" :commit="commit" :merge-request="job.merge_request" />
+ <commit-block
+ class="gl-py-4 gl-border-b gl-border-gray-50"
+ :commit="commit"
+ :merge-request="job.merge_request"
+ />
<stages-dropdown
v-if="job.pipeline"
- class="block gl-mb-2"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
:stages="stages"
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
index 5b1bf354fd4..d7726b952de 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
@@ -39,8 +39,8 @@ export default {
};
</script>
<template>
- <p class="build-sidebar-item gl-mb-2">
- <b v-if="hasTitle" class="gl-display-flex">{{ title }}:</b>
+ <p class="build-sidebar-item gl-line-height-normal gl-display-flex gl-mb-3">
+ <b v-if="hasTitle" class="gl-mr-3">{{ title }}:</b>
<gl-link
v-if="path"
:href="path"
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
index 77e3ecb9b3c..f757a3bcf00 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
@@ -6,7 +6,6 @@ import { createAlert } from '~/alert';
import { TYPENAME_COMMIT_STATUS } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { JOB_GRAPHQL_ERRORS, forwardDeploymentFailureModalId, PASSED_STATUS } from '~/ci/constants';
import GetJob from '../../graphql/queries/get_job.query.graphql';
import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
@@ -20,7 +19,6 @@ export default {
eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'),
newIssue: __('New issue'),
retryJobLabel: s__('Job|Retry'),
- toggleSidebar: __('Toggle Sidebar'),
runAgainJobButtonLabel: s__('Job|Run again'),
},
forwardDeploymentFailureModalId,
@@ -30,7 +28,6 @@ export default {
components: {
GlButton,
JobSidebarRetryButton,
- TooltipOnTruncate,
},
inject: ['projectPath'],
apollo: {
@@ -85,6 +82,15 @@ export default {
retryButtonCategory() {
return this.restJob.status && this.restJob.recoverable ? 'primary' : 'secondary';
},
+ jobHasPath() {
+ return Boolean(
+ this.restJob.erase_path ||
+ this.restJob.new_issue_path ||
+ this.restJob.terminal_path ||
+ this.restJob.retry_path ||
+ this.restJob.cancel_path,
+ );
+ },
},
methods: {
...mapActions(['toggleSidebar']),
@@ -93,73 +99,74 @@ export default {
</script>
<template>
- <div>
- <tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-mt-0 gl-mb-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4>
- </tooltip-on-truncate>
- <div class="gl-display-flex gl-gap-3">
- <gl-button
- v-if="restJob.erase_path"
- v-gl-tooltip.bottom
- :title="$options.i18n.eraseLogButtonLabel"
- :aria-label="$options.i18n.eraseLogButtonLabel"
- :href="restJob.erase_path"
- :data-confirm="$options.i18n.eraseLogConfirmText"
- data-testid="job-log-erase-link"
- data-confirm-btn-variant="danger"
- data-method="post"
- icon="remove"
- />
- <gl-button
- v-if="restJob.new_issue_path"
- v-gl-tooltip.bottom
- :href="restJob.new_issue_path"
- :title="$options.i18n.newIssue"
- :aria-label="$options.i18n.newIssue"
- category="secondary"
- variant="confirm"
- data-testid="job-new-issue"
- icon="issue-new"
- />
- <gl-button
- v-if="restJob.terminal_path"
- v-gl-tooltip.bottom
- :href="restJob.terminal_path"
- :title="$options.i18n.debug"
- :aria-label="$options.i18n.debug"
- target="_blank"
- icon="external-link"
- data-testid="terminal-link"
- />
- <job-sidebar-retry-button
- v-if="canShowJobRetryButton"
- v-gl-tooltip.bottom
- :title="buttonTitle"
- :aria-label="buttonTitle"
- :is-manual-job="isManualJob"
- :category="retryButtonCategory"
- :href="restJob.retry_path"
- :modal-id="$options.forwardDeploymentFailureModalId"
- variant="confirm"
- data-testid="retry-button"
- @updateVariablesClicked="$emit('updateVariables')"
- />
- <gl-button
- v-if="restJob.cancel_path"
- v-gl-tooltip.bottom
- :title="$options.i18n.cancelJobButtonLabel"
- :aria-label="$options.i18n.cancelJobButtonLabel"
- :href="restJob.cancel_path"
- variant="danger"
- icon="cancel"
- data-method="post"
- data-testid="cancel-button"
- rel="nofollow"
- />
+ <div class="gl-py-3!">
+ <div class="gl-display-flex gl-justify-content-space-between gl-gap-3">
+ <div class="gl-display-flex gl-gap-3">
+ <template v-if="jobHasPath">
+ <gl-button
+ v-if="restJob.erase_path"
+ v-gl-tooltip.bottom
+ :title="$options.i18n.eraseLogButtonLabel"
+ :aria-label="$options.i18n.eraseLogButtonLabel"
+ :href="restJob.erase_path"
+ :data-confirm="$options.i18n.eraseLogConfirmText"
+ data-testid="job-log-erase-link"
+ data-confirm-btn-variant="danger"
+ data-method="post"
+ icon="remove"
+ />
+ <gl-button
+ v-if="restJob.new_issue_path"
+ v-gl-tooltip.bottom
+ :href="restJob.new_issue_path"
+ :title="$options.i18n.newIssue"
+ :aria-label="$options.i18n.newIssue"
+ category="secondary"
+ variant="confirm"
+ data-testid="job-new-issue"
+ icon="issue-new"
+ />
+ <gl-button
+ v-if="restJob.terminal_path"
+ v-gl-tooltip.bottom
+ :href="restJob.terminal_path"
+ :title="$options.i18n.debug"
+ :aria-label="$options.i18n.debug"
+ target="_blank"
+ icon="external-link"
+ data-testid="terminal-link"
+ />
+ <job-sidebar-retry-button
+ v-if="canShowJobRetryButton"
+ v-gl-tooltip.bottom
+ :title="buttonTitle"
+ :aria-label="buttonTitle"
+ :is-manual-job="isManualJob"
+ :category="retryButtonCategory"
+ :href="restJob.retry_path"
+ :modal-id="$options.forwardDeploymentFailureModalId"
+ variant="confirm"
+ data-testid="retry-button"
+ @updateVariablesClicked="$emit('updateVariables')"
+ />
+ <gl-button
+ v-if="restJob.cancel_path"
+ v-gl-tooltip.bottom
+ :title="$options.i18n.cancelJobButtonLabel"
+ :aria-label="$options.i18n.cancelJobButtonLabel"
+ :href="restJob.cancel_path"
+ variant="danger"
+ icon="cancel"
+ data-method="post"
+ data-testid="cancel-button"
+ rel="nofollow"
+ />
+ </template>
+ </div>
<gl-button
:aria-label="$options.i18n.toggleSidebar"
category="secondary"
- class="gl-md-display-none gl-ml-2"
+ class="gl-lg-display-none"
icon="chevron-double-lg-right"
@click="toggleSidebar"
/>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
index ebef3ecaa3f..f04987a87b5 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
@@ -44,14 +44,10 @@ export default {
this.job.finished_at ||
this.job.erased_at ||
this.job.queued_duration ||
- this.job.id ||
this.job.runner ||
this.job.coverage,
);
},
- jobId() {
- return this.job?.id ? `#${this.job.id}` : '';
- },
runnerId() {
const { id, short_sha: token, description } = this.job.runner;
@@ -87,7 +83,6 @@ export default {
RUNNER: __('Runner'),
TAGS: __('Tags'),
TIMEOUT: __('Timeout'),
- ID: __('Job ID'),
},
TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', {
anchor: 'set-a-limit-for-how-long-jobs-can-run',
@@ -113,7 +108,6 @@ export default {
data-testid="job-timeout"
:title="$options.i18n.TIMEOUT"
/>
- <detail-row v-if="job.id" :value="jobId" :title="$options.i18n.ID" />
<detail-row
v-if="job.runner"
:value="runnerId"
diff --git a/app/assets/javascripts/ci/job_details/components/stuck_block.vue b/app/assets/javascripts/ci/job_details/components/stuck_block.vue
index 8c73f09daea..b8ff0b032cc 100644
--- a/app/assets/javascripts/ci/job_details/components/stuck_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/stuck_block.vue
@@ -78,7 +78,7 @@ export default {
</template>
</gl-sprintf>
<template v-if="stuckData.showTags">
- <gl-badge v-for="tag in tags" :key="tag" variant="info">
+ <gl-badge v-for="tag in tags" :key="tag" size="sm" variant="info">
{{ tag }}
</gl-badge>
</template>
diff --git a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
index 7fb887b2dd4..3a27a9a62a3 100644
--- a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
@@ -7,5 +7,4 @@ fragment BaseCiJob on CiJob {
...ManualCiVariable
}
}
- __typename
}
diff --git a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
index 0479df7bc4c..e560a2f29b6 100644
--- a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
@@ -1,5 +1,4 @@
fragment ManualCiVariable on CiVariable {
- __typename
id
key
value
diff --git a/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
index cd66a30ce63..b7c93c2830a 100644
--- a/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
@@ -1,6 +1,6 @@
#import "~/ci/job_details/graphql/fragments/ci_job.fragment.graphql"
-mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
+mutation retryJobWithVariables($id: CiProcessableID!, $variables: [CiVariableInput!]) {
jobRetry(input: { id: $id, variables: $variables }) {
job {
...BaseCiJob
diff --git a/app/assets/javascripts/ci/job_details/index.js b/app/assets/javascripts/ci/job_details/index.js
index 5a1ecf2fff3..20235015ce6 100644
--- a/app/assets/javascripts/ci/job_details/index.js
+++ b/app/assets/javascripts/ci/job_details/index.js
@@ -13,11 +13,11 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
-const initializeJobPage = (element) => {
- const store = createStore();
-
- // Let's start initializing the store (i.e. fetching data) right away
- store.dispatch('init', element.dataset);
+export const initJobDetails = () => {
+ const el = document.getElementById('js-job-page');
+ if (!el) {
+ return null;
+ }
const {
artifactHelpUrl,
@@ -26,27 +26,27 @@ const initializeJobPage = (element) => {
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
- logState,
buildStatus,
projectPath,
retryOutdatedJobDocsUrl,
aiRootCauseAnalysisAvailable,
- } = element.dataset;
+ } = el.dataset;
+
+ // init store to start fetching log
+ const store = createStore();
+ store.dispatch('init', { endpoint, pagePath });
return new Vue({
- el: element,
+ el,
apolloProvider,
store,
- components: {
- JobApp,
- },
provide: {
projectPath,
retryOutdatedJobDocsUrl,
aiRootCauseAnalysisAvailable: parseBoolean(aiRootCauseAnalysisAvailable),
},
- render(createElement) {
- return createElement('job-app', {
+ render(h) {
+ return h(JobApp, {
props: {
artifactHelpUrl,
deploymentHelpUrl,
@@ -54,7 +54,6 @@ const initializeJobPage = (element) => {
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
- logState,
buildStatus,
projectPath,
},
@@ -62,8 +61,3 @@ const initializeJobPage = (element) => {
},
});
};
-
-export default () => {
- const jobElement = document.getElementById('js-job-page');
- initializeJobPage(jobElement);
-};
diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index 5137ebfeaa8..119f8259be7 100644
--- a/app/assets/javascripts/ci/job_details/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -130,7 +130,7 @@ export default {
},
jobName() {
- return sprintf(__('Job %{jobName}'), { jobName: this.job.name });
+ return sprintf(__('%{jobName}'), { jobName: this.job.name });
},
},
watch: {
@@ -195,7 +195,7 @@ export default {
},
updateSidebar() {
const breakpoint = bp.getBreakpointSize();
- if (breakpoint === 'xs' || breakpoint === 'sm') {
+ if (breakpoint === 'xs' || breakpoint === 'sm' || breakpoint === 'md') {
this.hideSidebar();
} else if (!this.isSidebarOpen) {
this.showSidebar();
@@ -224,7 +224,7 @@ export default {
<div class="build-page" data-testid="job-content">
<!-- Header Section -->
<header>
- <div class="build-header top-area">
+ <div class="build-header gl-display-flex">
<job-header
:status="job.status"
:time="headerTime"
@@ -290,11 +290,7 @@ export default {
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!-- job log -->
- <div
- v-if="hasJobLog && !showUpdateVariablesState"
- class="build-log-container gl-relative"
- :class="{ 'gl-mt-3': !job.archived }"
- >
+ <div v-if="hasJobLog && !showUpdateVariablesState" class="build-log-container gl-relative">
<log-top-bar
:class="{
'has-archived-block': job.archived,
@@ -332,18 +328,17 @@ export default {
<!-- EO empty state -->
<!-- EO Body Section -->
+
+ <sidebar
+ :class="{
+ 'right-sidebar-expanded': isSidebarOpen,
+ 'right-sidebar-collapsed': !isSidebarOpen,
+ }"
+ :artifact-help-url="artifactHelpUrl"
+ data-testid="job-sidebar"
+ @updateVariables="onUpdateVariables()"
+ />
</div>
</template>
-
- <sidebar
- v-if="shouldRenderContent"
- :class="{
- 'right-sidebar-expanded': isSidebarOpen,
- 'right-sidebar-collapsed': !isSidebarOpen,
- }"
- :artifact-help-url="artifactHelpUrl"
- data-testid="job-sidebar"
- @updateVariables="onUpdateVariables()"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ci/job_details/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index 33d83689e61..fa23589f7d6 100644
--- a/app/assets/javascripts/ci/job_details/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -15,17 +15,15 @@ import { __ } from '~/locale';
import { reportToSentry } from '~/ci/utils';
import * as types from './mutation_types';
-export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
- dispatch('setJobEndpoint', endpoint);
+export const init = ({ dispatch }, { endpoint, pagePath }) => {
dispatch('setJobLogOptions', {
- logState,
+ endpoint,
pagePath,
});
return dispatch('fetchJob');
};
-export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
export const setJobLogOptions = ({ commit }, options) => commit(types.SET_JOB_LOG_OPTIONS, options);
export const hideSidebar = ({ commit }) => commit(types.HIDE_SIDEBAR);
diff --git a/app/assets/javascripts/ci/job_details/store/mutation_types.js b/app/assets/javascripts/ci/job_details/store/mutation_types.js
index 4915a826b84..e125538317d 100644
--- a/app/assets/javascripts/ci/job_details/store/mutation_types.js
+++ b/app/assets/javascripts/ci/job_details/store/mutation_types.js
@@ -1,4 +1,3 @@
-export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT';
export const SET_JOB_LOG_OPTIONS = 'SET_JOB_LOG_OPTIONS';
export const HIDE_SIDEBAR = 'HIDE_SIDEBAR';
diff --git a/app/assets/javascripts/ci/job_details/store/mutations.js b/app/assets/javascripts/ci/job_details/store/mutations.js
index b7d7006ee61..fe6506bf8a5 100644
--- a/app/assets/javascripts/ci/job_details/store/mutations.js
+++ b/app/assets/javascripts/ci/job_details/store/mutations.js
@@ -3,13 +3,9 @@ import * as types from './mutation_types';
import { logLinesParser, updateIncrementalJobLog } from './utils';
export default {
- [types.SET_JOB_ENDPOINT](state, endpoint) {
- state.jobEndpoint = endpoint;
- },
-
[types.SET_JOB_LOG_OPTIONS](state, options = {}) {
state.jobLogEndpoint = options.pagePath;
- state.jobLogState = options.logState;
+ state.jobEndpoint = options.endpoint;
},
[types.HIDE_SIDEBAR](state) {
diff --git a/app/assets/javascripts/ci/job_details/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index bc76901026d..b18a3fa162d 100644
--- a/app/assets/javascripts/ci/job_details/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
@@ -19,20 +19,17 @@ export const parseLine = (line = {}, lineNumber) => ({
* @param Number lineNumber
*/
export const parseHeaderLine = (line = {}, lineNumber, hash) => {
+ let isClosed = parseBoolean(line.section_options?.collapsed);
+
// if a hash is present in the URL then we ensure
// all sections are visible so we can scroll to the hash
// in the DOM
if (hash) {
- return {
- isClosed: false,
- isHeader: true,
- line: parseLine(line, lineNumber),
- lines: [],
- };
+ isClosed = false;
}
return {
- isClosed: parseBoolean(line.section_options?.collapsed),
+ isClosed,
isHeader: true,
line: parseLine(line, lineNumber),
lines: [],
@@ -80,27 +77,28 @@ export const isCollapsibleSection = (acc = [], last = {}, section = {}) =>
section.section === last.line.section;
/**
- * Returns the lineNumber of the last line in
- * a parsed log
+ * Returns the next line number in the parsed log
*
* @param Array acc
* @returns Number
*/
-export const getIncrementalLineNumber = (acc) => {
- let lineNumberValue;
- const lastIndex = acc.length - 1;
- const lastElement = acc[lastIndex];
+export const getNextLineNumber = (acc) => {
+ if (!acc?.length) {
+ return 1;
+ }
+
+ const lastElement = acc[acc.length - 1];
const nestedLines = lastElement.lines;
if (lastElement.isHeader && !nestedLines.length && lastElement.line) {
- lineNumberValue = lastElement.line.lineNumber;
- } else if (lastElement.isHeader && nestedLines.length) {
- lineNumberValue = nestedLines[nestedLines.length - 1].lineNumber;
- } else {
- lineNumberValue = lastElement.lineNumber;
+ return lastElement.line.lineNumber + 1;
}
- return lineNumberValue === 0 ? 1 : lineNumberValue + 1;
+ if (lastElement.isHeader && nestedLines.length) {
+ return nestedLines[nestedLines.length - 1].lineNumber + 1;
+ }
+
+ return lastElement.lineNumber + 1;
};
/**
@@ -118,32 +116,29 @@ export const getIncrementalLineNumber = (acc) => {
* @param Array accumulator
* @returns Array parsed log lines
*/
-export const logLinesParser = (lines = [], accumulator = [], hash = '') =>
- lines.reduce(
- (acc, line, index) => {
- const lineNumber = accumulator.length > 0 ? getIncrementalLineNumber(acc) : index;
-
- const last = acc[acc.length - 1];
-
- // If the object is an header, we parse it into another structure
- if (line.section_header) {
- acc.push(parseHeaderLine(line, lineNumber, hash));
- } else if (isCollapsibleSection(acc, last, line)) {
- // if the object belongs to a nested section, we append it to the new `lines` array of the
- // previously formatted header
- last.lines.push(parseLine(line, lineNumber));
- } else if (line.section_duration) {
- // if the line has section_duration, we look for the correct header to add it
- addDurationToHeader(acc, line);
- } else {
- // otherwise it's a regular line
- acc.push(parseLine(line, lineNumber));
- }
+export const logLinesParser = (lines = [], prevLogLines = [], hash = '') =>
+ lines.reduce((acc, line) => {
+ const lineNumber = getNextLineNumber(acc);
+
+ const last = acc[acc.length - 1];
+
+ // If the object is an header, we parse it into another structure
+ if (line.section_header) {
+ acc.push(parseHeaderLine(line, lineNumber, hash));
+ } else if (isCollapsibleSection(acc, last, line)) {
+ // if the object belongs to a nested section, we append it to the new `lines` array of the
+ // previously formatted header
+ last.lines.push(parseLine(line, lineNumber));
+ } else if (line.section_duration) {
+ // if the line has section_duration, we look for the correct header to add it
+ addDurationToHeader(acc, line);
+ } else {
+ // otherwise it's a regular line
+ acc.push(parseLine(line, lineNumber));
+ }
- return acc;
- },
- [...accumulator],
- );
+ return acc;
+ }, prevLogLines);
/**
* Finds the repeated offset, removes the old one
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
index 609f2790869..3ad2582e36b 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
@@ -7,7 +7,7 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import {
@@ -133,7 +133,7 @@ export default {
variables: { id: this.job.id },
});
if (errors.length > 0) {
- reportMessageToSentry(this.$options.name, errors.join(', '), {});
+ reportToSentry(this.$options.name, new Error(errors.join(', ')));
this.showToastMessage();
} else if (redirect) {
// Retry and Play actions redirect to job detail view
@@ -143,7 +143,7 @@ export default {
eventHub.$emit('jobActionPerformed');
}
} catch (failure) {
- reportMessageToSentry(this.$options.name, failure, {});
+ reportToSentry(this.$options.name, failure);
this.showToastMessage();
}
},
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
index b435eb283fd..fbdfc7c9c6a 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
@@ -35,9 +35,6 @@ export default {
jobRef() {
return this.job?.refName;
},
- jobRefPath() {
- return this.job?.refPath;
- },
jobTags() {
return this.job.tags;
},
@@ -72,61 +69,60 @@ export default {
<template>
<div>
<div class="gl-text-truncate gl-p-3 gl-mt-n3 gl-mx-n3 gl-mb-n2">
- <gl-link
- v-if="canReadJob"
- class="gl-text-blue-600!"
- :href="jobPath"
- data-testid="job-id-link"
- >
- {{ jobId }}
- </gl-link>
-
- <span v-else data-testid="job-id-limited-access">{{ jobId }}</span>
-
<gl-icon
v-if="jobStuck"
v-gl-tooltip="$options.i18n.stuckText"
name="warning"
:size="$options.iconSize"
+ class="gl-mr-2"
data-testid="stuck-icon"
/>
- <div
- class="gl-display-flex gl-text-gray-700 gl-align-items-center gl-lg-justify-content-start gl-justify-content-end gl-mt-2"
+ <gl-link
+ v-if="canReadJob"
+ class="gl-text-blue-600!"
+ :href="jobPath"
+ data-testid="job-id-link"
>
- <div
- v-if="jobRef"
- class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate"
- >
- <gl-icon
- v-if="createdByTag"
- name="label"
- :size="$options.iconSize"
- data-testid="label-icon"
- />
- <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
- <gl-link
- class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
- :href="job.refPath"
- data-testid="job-ref"
- >{{ job.refName }}</gl-link
- >
- </div>
+ <span class="gl-text-truncate">
+ <span data-testid="job-name">{{ jobId }}: {{ job.name }}</span>
+ </span>
+ </gl-link>
- <span v-else>{{ __('none') }}</span>
- <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
- <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
- <gl-link
- class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
- :href="job.commitPath"
- data-testid="job-sha"
- >{{ job.shortSha }}</gl-link
- >
- </div>
+ <span v-else data-testid="job-id-limited-access">{{ jobId }}: {{ job.name }}</span>
+ </div>
+
+ <div
+ class="gl-display-flex gl-text-gray-700 gl-align-items-center gl-lg-justify-content-start gl-justify-content-end gl-mt-1"
+ >
+ <div v-if="jobRef" class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-26 gl-text-truncate">
+ <gl-icon
+ v-if="createdByTag"
+ name="label"
+ :size="$options.iconSize"
+ data-testid="label-icon"
+ />
+ <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
+ <gl-link
+ class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
+ :href="job.refPath"
+ data-testid="job-ref"
+ >{{ job.refName }}</gl-link
+ >
+ </div>
+ <span v-else>{{ __('none') }}</span>
+ <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
+ <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
+ <gl-link
+ class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
+ :href="job.commitPath"
+ data-testid="job-sha"
+ >{{ job.shortSha }}</gl-link
+ >
</div>
</div>
- <div>
+ <div class="gl-mt-2">
<gl-badge
v-for="tag in jobTags"
:key="tag"
@@ -136,7 +132,6 @@ export default {
>
{{ tag }}
</gl-badge>
-
<gl-badge
v-if="triggered"
variant="info"
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
index 18d68ee8a29..945674153c4 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
@@ -1,8 +1,12 @@
<script>
import { GlAvatar, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export default {
+ i18n: {
+ stageLabel: s__('Jobs|Stage'),
+ },
components: {
GlAvatar,
GlLink,
@@ -36,21 +40,22 @@ export default {
<template>
<div>
- <div class="gl-p-3 gl-mt-n3">
- <gl-link
- class="gl-text-truncate gl-ml-n3 gl-text-gray-500!"
- :href="pipelinePath"
- data-testid="pipeline-id"
- >
+ <div class="gl-p-3 gl-mt-n3 gl-mx-n3">
+ <gl-link class="gl-text-truncate" :href="pipelinePath" data-testid="pipeline-id">
{{ pipelineId }}
</gl-link>
+
+ <span class="gl-text-secondary">
+ <span>{{ __('created by') }}</span>
+ <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
+ <gl-avatar :src="pipelineUserAvatar" :size="16" />
+ </gl-link>
+ <span v-else>{{ __('API') }}</span>
+ </span>
</div>
- <div class="gl-font-sm gl-text-secondary gl-mt-n2">
- <span>{{ __('created by') }}</span>
- <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
- <gl-avatar :src="pipelineUserAvatar" :size="16" />
- </gl-link>
- <span v-else>{{ __('API') }}</span>
+
+ <div v-if="job.stage" class="gl-text-truncate gl-font-sm gl-text-secondary gl-mt-1">
+ <span data-testid="job-stage-name">{{ $options.i18n.stageLabel }}: {{ job.stage.name }}</span>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
index dbf1dfe7a29..a2b6a430138 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
@@ -1,12 +1,14 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { formatTime } from '~/lib/utils/datetime_utility';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
iconSize: 12,
components: {
+ CiBadgeLink,
GlIcon,
TimeAgoTooltip,
},
@@ -36,17 +38,16 @@ export default {
<template>
<div>
- <div v-if="duration" data-testid="job-duration">
- <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
- {{ durationFormatted }}
- </div>
- <div
- v-if="finishedTime"
- :class="{ 'gl-mt-2': hasDurationAndFinishedTime }"
- data-testid="job-finished-time"
- >
- <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
- <time-ago-tooltip :time="finishedTime" />
+ <ci-badge-link :status="job.detailedStatus" />
+ <div class="gl-font-sm gl-text-secondary gl-mt-2 gl-ml-3">
+ <div v-if="duration" data-testid="job-duration">
+ <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
+ {{ durationFormatted }}
+ </div>
+ <div v-if="finishedTime" data-testid="job-finished-time">
+ <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
+ <time-ago-tooltip :time="finishedTime" />
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
index 23100a3f3db..d81d19cfd52 100644
--- a/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
@@ -1,12 +1,11 @@
<script>
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
-import { DEFAULT_FIELDS } from '../constants';
+import { JOBS_DEFAULT_FIELDS } from '../constants';
import ActionsCell from './job_cells/actions_cell.vue';
-import DurationCell from './job_cells/duration_cell.vue';
+import StatusCell from './job_cells/status_cell.vue';
import JobCell from './job_cells/job_cell.vue';
import PipelineCell from './job_cells/pipeline_cell.vue';
@@ -16,13 +15,12 @@ export default {
},
components: {
ActionsCell,
- CiBadgeLink,
- DurationCell,
- GlTable,
+ StatusCell,
JobCell,
PipelineCell,
ProjectCell,
RunnerCell,
+ GlTable,
},
props: {
jobs: {
@@ -32,7 +30,7 @@ export default {
tableFields: {
type: Array,
required: false,
- default: () => DEFAULT_FIELDS,
+ default: () => JOBS_DEFAULT_FIELDS,
},
admin: {
type: Boolean,
@@ -64,7 +62,7 @@ export default {
</template>
<template #cell(status)="{ item }">
- <ci-badge-link :status="item.detailedStatus" />
+ <status-cell :job="item" />
</template>
<template #cell(job)="{ item }">
@@ -75,28 +73,20 @@ export default {
<pipeline-cell :job="item" />
</template>
- <template v-if="admin" #cell(project)="{ item }">
- <project-cell :job="item" />
- </template>
-
- <template v-if="admin" #cell(runner)="{ item }">
- <runner-cell :job="item" />
- </template>
-
<template #cell(stage)="{ item }">
<div class="gl-text-truncate">
- <span v-if="item.stage" data-testid="job-stage-name">{{ item.stage.name }}</span>
+ <span v-if="item.stage" data-testid="job-stage-name" class="gl-text-secondary">{{
+ item.stage.name
+ }}</span>
</div>
</template>
- <template #cell(name)="{ item }">
- <div class="gl-text-truncate">
- <span data-testid="job-name">{{ item.name }}</span>
- </div>
+ <template v-if="admin" #cell(project)="{ item }">
+ <project-cell :job="item" />
</template>
- <template #cell(duration)="{ item }">
- <duration-cell :job="item" />
+ <template v-if="admin" #cell(runner)="{ item }">
+ <runner-cell :job="item" />
</template>
<template #cell(coverage)="{ item }">
diff --git a/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
index d2cd27be034..7effb8fe239 100644
--- a/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
@@ -29,6 +29,7 @@ export default {
:title="$options.i18n.title"
:description="$options.i18n.description"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:primary-button-link="pipelineEditorPath"
:primary-button-text="$options.i18n.buttonText"
data-testid="jobs-empty-state"
diff --git a/app/assets/javascripts/ci/jobs_page/constants.js b/app/assets/javascripts/ci/jobs_page/constants.js
index 1b572e60c58..dec355ddff6 100644
--- a/app/assets/javascripts/ci/jobs_page/constants.js
+++ b/app/assets/javascripts/ci/jobs_page/constants.js
@@ -29,6 +29,7 @@ export const PLAY_JOB_CONFIRMATION_MESSAGE = s__(
export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?');
/* Table constants */
+/* There is another field list based on this one in app/assets/javascripts/ci/admin/jobs_table/constants.js */
export const DEFAULT_FIELDS = [
{
key: 'status',
@@ -38,7 +39,7 @@ export const DEFAULT_FIELDS = [
{
key: 'job',
label: __('Job'),
- columnClass: 'gl-w-20p',
+ columnClass: 'gl-w-quarter',
},
{
key: 'pipeline',
@@ -51,16 +52,6 @@ export const DEFAULT_FIELDS = [
columnClass: 'gl-w-10p',
},
{
- key: 'name',
- label: __('Name'),
- columnClass: 'gl-w-15p',
- },
- {
- key: 'duration',
- label: __('Duration'),
- columnClass: 'gl-w-15p',
- },
- {
key: 'coverage',
label: __('Coverage'),
tdClass: 'gl-display-none! gl-lg-display-table-cell!',
@@ -69,8 +60,10 @@ export const DEFAULT_FIELDS = [
{
key: 'actions',
label: '',
+ tdClass: 'gl-text-right',
columnClass: 'gl-w-10p',
},
];
+export const JOBS_DEFAULT_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'stage');
export const JOBS_TAB_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'pipeline');
diff --git a/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
index 6e51f9a20fa..077c8e31749 100644
--- a/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
@@ -1,6 +1,6 @@
#import "../fragments/job.fragment.graphql"
-mutation retryJob($id: CiBuildID!) {
+mutation retryJob($id: CiProcessableID!) {
jobRetry(input: { id: $id }) {
job {
...Job
diff --git a/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql b/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
index 022d461dbec..f6de6cde9d0 100644
--- a/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
+++ b/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
@@ -1,4 +1,4 @@
-mutation retryMrFailedJob($id: CiBuildID!) {
+mutation retryMrFailedJob($id: CiProcessableID!) {
jobRetry(input: { id: $id }) {
errors
}
diff --git a/app/assets/javascripts/ci/pipeline_details/constants.js b/app/assets/javascripts/ci/pipeline_details/constants.js
index bf312e66144..70b758ae6b0 100644
--- a/app/assets/javascripts/ci/pipeline_details/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/constants.js
@@ -23,8 +23,6 @@ export const PARSE_FAILURE = 'parse_failure';
export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
-export const CHILD_VIEW = 'child';
-
// Pipeline tabs
export const pipelineTabName = 'graph';
diff --git a/app/assets/javascripts/ci/pipeline_details/dag/dag.vue b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
index 5415340c956..fb8e5d679b7 100644
--- a/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
+++ b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
@@ -220,6 +220,7 @@ export default {
<gl-empty-state
v-else-if="hasNoDependentJobs"
:svg-path="emptyDagSvgPath"
+ :svg-height="null"
:title="$options.emptyStateTexts.title"
>
<template #description>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
index 7538ad87af8..ec8f30e94b4 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
@@ -65,7 +65,7 @@ export default {
<div
:id="computedJobId"
class="ci-job-dropdown-container dropdown dropright"
- data-qa-selector="job_dropdown_container"
+ data-testid="job-dropdown-container"
>
<button
type="button"
@@ -90,7 +90,7 @@ export default {
<ul
class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"
- data-qa-selector="jobs_dropdown_menu"
+ data-testid="jobs-dropdown-menu"
>
<li class="scrollable-menu">
<ul>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
index 4298052d1c0..bb36ac8b6ab 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
@@ -5,7 +5,7 @@ import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import ActionComponent from '../../../common/private/job_action_component.vue';
import JobNameComponent from '../../../common/private/job_name_component.vue';
import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from '../constants';
@@ -58,7 +58,7 @@ export default {
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
- CiIcon,
+ CiBadgeLink,
GlBadge,
GlForm,
GlFormCheckbox,
@@ -312,7 +312,6 @@ export default {
<div
:id="computedJobId"
class="ci-job-component gl-display-flex gl-justify-content-space-between gl-pipeline-job-width"
- data-qa-selector="job_item_container"
>
<component
:is="nameComponent"
@@ -326,12 +325,11 @@ export default {
:href="detailsPath"
class="js-pipeline-graph-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full"
:data-testid="testId"
- data-qa-selector="job_link"
@click="jobItemClick"
@mouseout="hideTooltips"
>
<div class="gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-icon :size="24" :status="job.status" class="gl-line-height-0" />
+ <ci-badge-link :status="job.status" size="md" :show-text="false" :use-link="false" />
<div class="gl-pl-3 gl-pr-3 gl-display-flex gl-flex-direction-column gl-pipeline-job-width">
<div class="gl-text-truncate gl-pr-9 gl-line-height-normal">{{ job.name }}</div>
<div
@@ -343,7 +341,13 @@ export default {
</div>
</div>
</div>
- <gl-badge v-if="isBridge" class="gl-mt-3" variant="info" size="sm">
+ <gl-badge
+ v-if="isBridge"
+ class="gl-mt-3"
+ variant="info"
+ size="sm"
+ data-testid="job-bridge-badge"
+ >
{{ $options.i18n.bridgeBadgeText }}
</gl-badge>
</component>
@@ -356,7 +360,6 @@ export default {
class="gl-mr-1"
:should-trigger-click="shouldTriggerActionClick"
:with-confirmation-modal="withConfirmationModal"
- data-qa-selector="job_action_button"
@actionButtonClicked="handleConfirmationModalPreferences"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
@showActionConfirmationModal="showActionConfirmationModal"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
index d6adaf78da4..5960eea5b4f 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
@@ -13,7 +13,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import CancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { reportToSentry } from '~/ci/utils';
import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants';
@@ -22,7 +22,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiIcon,
+ CiBadgeLink,
GlBadge,
GlButton,
GlLink,
@@ -233,7 +233,7 @@ export default {
ref="linkedPipeline"
class="gl-h-full gl-display-flex! gl-px-2"
:class="flexDirection"
- data-qa-selector="linked_pipeline_container"
+ data-testid="linked-pipeline-container"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
@@ -242,16 +242,19 @@ export default {
</gl-tooltip>
<div class="gl-bg-white gl-border gl-p-3 gl-rounded-lg gl-w-full" :class="cardClasses">
<div class="gl-display-flex gl-gap-x-3">
- <ci-icon v-if="!pipelineIsLoading" :status="pipelineStatus" :size="24" />
+ <ci-badge-link
+ v-if="!pipelineIsLoading"
+ :status="pipelineStatus"
+ size="md"
+ :show-text="false"
+ :use-link="false"
+ class="gl-align-self-start"
+ />
<div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
<div
class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
>
- <span
- class="gl-text-truncate"
- data-testid="downstream-title"
- data-qa-selector="downstream_title_content"
- >
+ <span class="gl-text-truncate" data-testid="downstream-title-content">
{{ downstreamTitle }}
</span>
<div class="gl-text-truncate">
@@ -294,7 +297,6 @@ export default {
:icon="expandedIcon"
:aria-label="expandBtnText"
data-testid="expand-pipeline-button"
- data-qa-selector="expand_linked_pipeline_button"
@mouseover="setExpandBtnActiveState(true)"
@mouseout="setExpandBtnActiveState(false)"
@focus="setExpandBtnActiveState(true)"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
index 1401bdba5ca..6030adc96ad 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
@@ -179,6 +179,7 @@ export default {
{ 'gl-opacity-3': isFadedOut(group.name) },
'gl-transition-duration-slow gl-transition-timing-function-ease',
]"
+ data-testid="job-item-container"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
@setSkipRetryModal="$emit('setSkipRetryModal')"
/>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
index bd7325f7925..a6e7a645442 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
@@ -6,7 +6,7 @@ import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '~/ci/pipeline_details/constants';
import getPipelineQuery from '~/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql';
-import { reportToSentry, reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import DismissPipelineGraphCallout from './graphql/mutations/dismiss_pipeline_notification.graphql';
import {
ACTION_FAILURE,
@@ -156,17 +156,7 @@ export default {
error(err) {
this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
- reportMessageToSentry(
- this.$options.name,
- `| type: ${LOAD_FAILURE} , info: ${JSON.stringify(err)}`,
- {
- graphViewType: this.graphViewType,
- graphqlResourceEtag: this.graphqlResourceEtag,
- metricsPath: this.metricsPath,
- projectPath: this.pipelineProjectPath,
- pipelineIid: this.pipelineIid,
- },
- );
+ reportToSentry(this.$options.name, new Error(err));
},
result({ data, error }) {
const stages = data?.project?.pipeline?.stages?.nodes || [];
diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index 3a6a655bfa6..51a68f6619a 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -396,18 +396,14 @@ export default {
</div>
</gl-alert>
<gl-loading-icon v-if="loading" class="gl-text-left" size="lg" />
- <div
- v-else
- class="gl-display-flex gl-justify-content-space-between gl-flex-wrap"
- data-qa-selector="pipeline_details_header"
- >
+ <div v-else class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
<div>
<h3 v-if="name" class="gl-mt-0 gl-mb-3" data-testid="pipeline-name">{{ name }}</h3>
<h3 v-else class="gl-mt-0 gl-mb-3" data-testid="pipeline-commit-title">
{{ commitTitle }}
</h3>
<div>
- <ci-badge-link :status="detailedStatus" />
+ <ci-badge-link :status="detailedStatus" class="gl-display-inline-block gl-mb-3" />
<div class="gl-ml-2 gl-mb-3 gl-display-inline-block gl-h-6">
<gl-link
v-if="user"
@@ -423,7 +419,7 @@ export default {
<template #link="{ content }">
<gl-link
:href="commitPath"
- class="gl-bg-blue-50 gl-rounded-base gl-px-2 gl-mx-2"
+ class="commit-sha-container"
data-testid="commit-link"
target="_blank"
>
@@ -431,6 +427,8 @@ export default {
</gl-link>
</template>
</gl-sprintf>
+ </div>
+ <div class="gl-display-inline-block gl-mb-3">
<clipboard-button
:text="shortId"
category="tertiary"
@@ -449,123 +447,127 @@ export default {
</div>
<div v-safe-html="refText" class="gl-mb-3" data-testid="pipeline-ref-text"></div>
<div>
- <gl-badge
- v-if="badges.schedule"
- v-gl-tooltip
- :title="$options.i18n.scheduleBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.scheduleBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.child"
- v-gl-tooltip
- :title="$options.i18n.childBadgeTooltip"
- variant="info"
- size="sm"
- >
- <gl-sprintf :message="$options.i18n.childBadgeText">
- <template #link="{ content }">
- <gl-link :href="paths.triggeredByPath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-badge>
- <gl-badge
- v-if="badges.latest"
- v-gl-tooltip
- :title="$options.i18n.latestBadgeTooltip"
- variant="success"
- size="sm"
- >
- {{ $options.i18n.latestBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.mergeTrainPipeline"
- v-gl-tooltip
- :title="$options.i18n.mergeTrainBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.mergeTrainBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.invalid"
- v-gl-tooltip
- :title="yamlErrors"
- variant="danger"
- size="sm"
- >
- {{ $options.i18n.invalidBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.failed"
- v-gl-tooltip
- :title="failureReason"
- variant="danger"
- size="sm"
- >
- {{ $options.i18n.failedBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.autoDevops"
- v-gl-tooltip
- :title="$options.i18n.autoDevopsBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.autoDevopsBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.detached"
- v-gl-tooltip
- :title="$options.i18n.detachedBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.detachedBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.stuck"
- v-gl-tooltip
- :title="$options.i18n.stuckBadgeTooltip"
- variant="warning"
- size="sm"
- >
- {{ $options.i18n.stuckBadgeText }}
- </gl-badge>
- <span
- v-gl-tooltip
- :title="$options.i18n.totalJobsTooltip"
- class="gl-ml-2"
- data-testid="total-jobs"
- >
- <gl-icon name="pipeline" />
- {{ totalJobsText }}
- </span>
- <span
- v-if="showComputeMinutes"
- v-gl-tooltip
- :title="$options.i18n.computeMinutesTooltip"
- class="gl-ml-2"
- data-testid="compute-minutes"
- >
- <gl-icon name="quota" />
- {{ computeMinutes }}
- </span>
- <span v-if="inProgress" class="gl-ml-2" data-testid="pipeline-running-text">
- <gl-icon name="timer" />
- {{ inProgressText }}
- </span>
- <span v-if="showDuration" class="gl-ml-2" data-testid="pipeline-duration-text">
- <gl-icon name="timer" />
- {{ durationText }}
- </span>
+ <div class="gl-display-inline-block gl-mb-3">
+ <gl-badge
+ v-if="badges.schedule"
+ v-gl-tooltip
+ :title="$options.i18n.scheduleBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.scheduleBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.child"
+ v-gl-tooltip
+ :title="$options.i18n.childBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ <gl-sprintf :message="$options.i18n.childBadgeText">
+ <template #link="{ content }">
+ <gl-link :href="paths.triggeredByPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-badge>
+ <gl-badge
+ v-if="badges.latest"
+ v-gl-tooltip
+ :title="$options.i18n.latestBadgeTooltip"
+ variant="success"
+ size="sm"
+ >
+ {{ $options.i18n.latestBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.mergeTrainPipeline"
+ v-gl-tooltip
+ :title="$options.i18n.mergeTrainBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.mergeTrainBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.invalid"
+ v-gl-tooltip
+ :title="yamlErrors"
+ variant="danger"
+ size="sm"
+ >
+ {{ $options.i18n.invalidBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.failed"
+ v-gl-tooltip
+ :title="failureReason"
+ variant="danger"
+ size="sm"
+ >
+ {{ $options.i18n.failedBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.autoDevops"
+ v-gl-tooltip
+ :title="$options.i18n.autoDevopsBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.autoDevopsBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.detached"
+ v-gl-tooltip
+ :title="$options.i18n.detachedBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.detachedBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.stuck"
+ v-gl-tooltip
+ :title="$options.i18n.stuckBadgeTooltip"
+ variant="warning"
+ size="sm"
+ >
+ {{ $options.i18n.stuckBadgeText }}
+ </gl-badge>
+ </div>
+ <div class="gl-display-inline-block">
+ <span
+ v-gl-tooltip
+ :title="$options.i18n.totalJobsTooltip"
+ class="gl-ml-2"
+ data-testid="total-jobs"
+ >
+ <gl-icon name="pipeline" />
+ {{ totalJobsText }}
+ </span>
+ <span
+ v-if="showComputeMinutes"
+ v-gl-tooltip
+ :title="$options.i18n.computeMinutesTooltip"
+ class="gl-ml-2"
+ data-testid="compute-minutes"
+ >
+ <gl-icon name="quota" />
+ {{ computeMinutes }}
+ </span>
+ <span v-if="inProgress" class="gl-ml-2" data-testid="pipeline-running-text">
+ <gl-icon name="timer" />
+ {{ inProgressText }}
+ </span>
+ <span v-if="showDuration" class="gl-ml-2" data-testid="pipeline-duration-text">
+ <gl-icon name="timer" />
+ {{ durationText }}
+ </span>
+ </div>
</div>
</div>
- <div class="gl-mt-5 gl-lg-mt-0">
+ <div class="gl-mt-5 gl-lg-mt-0 gl-display-flex gl-align-items-flex-start gl-gap-3">
<gl-button
v-if="canRetryPipeline"
v-gl-tooltip
@@ -588,7 +590,6 @@ export default {
:title="$options.BUTTON_TOOLTIP_CANCEL"
:loading="isCanceling"
:disabled="isCanceling"
- class="gl-ml-3"
variant="danger"
data-testid="cancel-pipeline"
@click="cancelPipeline()"
@@ -601,7 +602,6 @@ export default {
v-gl-modal="$options.modal.id"
:loading="isDeleting"
:disabled="isDeleting"
- class="gl-ml-3"
variant="danger"
category="secondary"
data-testid="delete-pipeline"
diff --git a/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
index 1955cc9b0ac..b60afe51dd2 100644
--- a/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
@@ -1,4 +1,4 @@
-mutation retryFailedJob($id: CiBuildID!) {
+mutation retryFailedJob($id: CiProcessableID!) {
jobRetry(input: { id: $id }) {
job {
id
diff --git a/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
index 53f755fda37..5d1f1ac770c 100644
--- a/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
@@ -52,14 +52,12 @@ export default {
});
eventHub.$on('postAction', this.postAction);
- eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('updateTable', this.updateTable);
eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
- eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('updateTable', this.updateTable);
eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
@@ -68,6 +66,15 @@ export default {
this.poll.stop();
},
methods: {
+ onCancelPipeline(pipeline) {
+ this.postAction(pipeline.cancel_path);
+ },
+ onRefreshPipelinesTable() {
+ this.updateTable();
+ },
+ onRetryPipeline(pipeline) {
+ this.postAction(pipeline.retry_path);
+ },
updateInternalState(parameters) {
this.poll.stop();
diff --git a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
index d38397e7479..8a7c3367fc1 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
@@ -31,10 +31,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
endpoint,
artifactsEndpoint,
artifactsEndpointPlaceholder,
- pipelineScheduleUrl,
- emptyStateSvgPath,
- errorStateSvgPath,
- noPipelinesSvgPath,
+ pipelineSchedulesPath,
newPipelinePath,
pipelineEditorPath,
suggestedCiTemplates,
@@ -55,13 +52,14 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
el,
apolloProvider,
provide: {
- pipelineEditorPath,
artifactsEndpoint,
artifactsEndpointPlaceholder,
- suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
- iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
fullPath,
+ iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
manualActionsLimit: 50,
+ pipelineEditorPath,
+ pipelineSchedulesPath,
+ suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
},
data() {
return {
@@ -77,22 +75,18 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
render(createElement) {
return createElement(Pipelines, {
props: {
- store: this.store,
- endpoint,
- pipelineScheduleUrl,
- emptyStateSvgPath,
- errorStateSvgPath,
- noPipelinesSvgPath,
- newPipelinePath,
canCreatePipeline: parseBoolean(canCreatePipeline),
- hasGitlabCi: parseBoolean(hasGitlabCi),
ciLintPath,
- resetCachePath,
- projectId,
defaultBranchName,
+ defaultVisibilityPipelineIdType: visibilityPipelineIdType,
+ endpoint,
+ hasGitlabCi: parseBoolean(hasGitlabCi),
+ newPipelinePath,
params: JSON.parse(params),
+ projectId,
registrationToken,
- defaultVisibilityPipelineIdType: visibilityPipelineIdType,
+ resetCachePath,
+ store: this.store,
},
});
},
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
index 58b5c0004e0..44cf11acfe2 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
@@ -7,7 +7,7 @@ import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.quer
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
@@ -25,7 +25,7 @@ export const i18n = {
export default {
i18n,
components: {
- CiIcon,
+ CiBadgeLink,
GlButton,
GlIcon,
GlLink,
@@ -156,7 +156,12 @@ export default {
<template v-else>
<div class="gl-text-truncate gl-md-max-w-50p gl-mr-1">
<a :href="status.detailsPath" class="gl-mr-auto">
- <ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" class="gl-mr-2" />
+ <ci-badge-link
+ :status="status"
+ size="md"
+ :show-text="false"
+ data-testid="pipeline-status-icon"
+ />
</a>
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
index bbe0f1fbefc..34640d49b80 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
@@ -13,7 +13,7 @@
*/
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { createAlert } from '~/alert';
import eventHub from '~/ci/event_hub';
import axios from '~/lib/utils/axios_utils';
@@ -33,7 +33,7 @@ export default {
positionFixed: true,
},
components: {
- CiIcon,
+ CiBadgeLink,
GlLoadingIcon,
GlDropdown,
LegacyJobItem,
@@ -126,14 +126,13 @@ export default {
@show="onShowDropdown"
>
<template #button-content>
- <ci-icon
- is-borderless
- is-interactive
- css-classes="gl-rounded-full"
- :is-active="isDropdownOpen"
- :size="24"
+ <ci-badge-link
:status="stage.status"
- class="gl-display-inline-flex gl-align-items-center gl-border gl-z-index-1"
+ size="md"
+ :show-text="false"
+ :show-tooltip="false"
+ :use-link="false"
+ class="gl-mb-0!"
/>
</template>
<div v-if="isLoading" class="gl--flex-center gl-p-2" data-testid="pipeline-stage-loading-state">
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
index 8567654a89e..cc703d29e23 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { accessValue } from './accessors/linked_pipelines_accessors';
/**
* Renders the upstream/downstream portions of the pipeline mini graph.
@@ -11,7 +11,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiIcon,
+ CiBadgeLink,
},
inject: {
dataMethod: {
@@ -99,24 +99,18 @@ export default {
}"
class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle"
>
- <a
+ <ci-badge-link
v-for="pipeline in linkedPipelinesTrimmed"
:key="pipeline.id"
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
- :href="pipeline.path"
+ :status="pipelineStatus(pipeline)"
+ size="md"
+ :show-text="false"
+ :show-tooltip="false"
:class="triggerButtonClass(pipeline)"
- class="linked-pipeline-mini-item gl-display-inline-flex gl-mr-2 gl-my-2 gl-rounded-full gl-vertical-align-middle"
+ class="linked-pipeline-mini-item gl-mb-0!"
data-testid="linked-pipeline-mini-item"
- >
- <ci-icon
- is-borderless
- is-interactive
- css-classes="gl-rounded-full"
- :size="24"
- :status="pipelineStatus(pipeline)"
- class="gl-align-items-center gl-border gl-display-inline-flex"
- />
- </a>
+ />
<a
v-if="shouldRenderCounter"
diff --git a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
index cc7d9bd2340..2f06b82bac0 100644
--- a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
@@ -438,8 +438,7 @@ export default {
v-for="(variable, index) in variables"
:key="variable.uniqueId"
class="gl-mb-3 gl-pb-2"
- data-testid="ci-variable-row"
- data-qa-selector="ci_variable_row_container"
+ data-testid="ci-variable-row-container"
>
<div
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
@@ -461,8 +460,7 @@ export default {
v-model="variable.key"
:placeholder="s__('CiVariables|Input variable key')"
:class="$options.formElementClasses"
- data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
+ data-testid="pipeline-form-ci-variable-key-field"
@change="addEmptyVariable(refFullName)"
/>
<gl-dropdown
@@ -471,12 +469,11 @@ export default {
:class="$options.formElementClasses"
class="gl-flex-grow-1 gl-mr-0!"
data-testid="pipeline-form-ci-variable-value-dropdown"
- data-qa-selector="ci_variable_value_dropdown"
>
<gl-dropdown-item
v-for="option in configVariablesWithDescription.options[variable.key]"
:key="option"
- data-qa-selector="ci_variable_value_dropdown_item"
+ data-testid="ci-variable-value-dropdown-item"
@click="setVariableAttribute(variable.key, 'value', option)"
>
{{ option }}
@@ -489,8 +486,7 @@ export default {
class="gl-mb-3"
:style="$options.textAreaStyle"
:no-resize="false"
- data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
+ data-testid="pipeline-form-ci-variable-value-field"
/>
<template v-if="variables.length > 1">
@@ -542,8 +538,7 @@ export default {
category="primary"
variant="confirm"
class="js-no-auto-disable gl-mr-3"
- data-qa-selector="run_pipeline_button"
- data-testid="run_pipeline_button"
+ data-testid="run-pipeline-button"
:disabled="submitted"
>{{ s__('Pipeline|Run pipeline') }}</gl-button
>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
index c993b65f6c0..386835d21d4 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
@@ -4,6 +4,7 @@ import {
GlBadge,
GlButton,
GlLoadingIcon,
+ GlPagination,
GlTabs,
GlTab,
GlSprintf,
@@ -16,12 +17,20 @@ import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline
import playPipelineScheduleMutation from '../graphql/mutations/play_pipeline_schedule.mutation.graphql';
import takeOwnershipMutation from '../graphql/mutations/take_ownership.mutation.graphql';
import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
-import { ALL_SCOPE } from '../constants';
+import { ALL_SCOPE, SCHEDULES_PER_PAGE } from '../constants';
import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
import TakeOwnershipModal from './take_ownership_modal.vue';
import DeletePipelineScheduleModal from './delete_pipeline_schedule_modal.vue';
import PipelineScheduleEmptyState from './pipeline_schedules_empty_state.vue';
+const defaultPagination = {
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ prevPageCursor: '',
+ nextPageCursor: '',
+ currentPage: 1,
+};
+
export default {
i18n: {
schedulesFetchError: s__('PipelineSchedules|There was a problem fetching pipeline schedules.'),
@@ -44,6 +53,7 @@ export default {
GlBadge,
GlButton,
GlLoadingIcon,
+ GlPagination,
GlTabs,
GlTab,
GlSprintf,
@@ -72,16 +82,22 @@ export default {
// we need to ensure we send null to the API when
// the scope is 'ALL'
status: this.scope === ALL_SCOPE ? null : this.scope,
+ first: this.pagination.first,
+ last: this.pagination.last,
+ prevPageCursor: this.pagination.prevPageCursor,
+ nextPageCursor: this.pagination.nextPageCursor,
};
},
update(data) {
- const { pipelineSchedules: { nodes: list = [], count } = {} } = data.project || {};
+ const { pipelineSchedules: { nodes: list = [], count, pageInfo = {} } = {} } =
+ data.project || {};
const currentUser = data.currentUser || {};
return {
list,
count,
currentUser,
+ pageInfo,
};
},
error() {
@@ -104,6 +120,9 @@ export default {
showDeleteModal: false,
showTakeOwnershipModal: false,
count: 0,
+ pagination: {
+ ...defaultPagination,
+ },
};
},
computed: {
@@ -144,6 +163,15 @@ export default {
showEmptyState() {
return !this.isLoading && this.schedulesCount === 0 && this.onAllTab;
},
+ showPagination() {
+ return this.schedules?.pageInfo?.hasNextPage || this.schedules?.pageInfo?.hasPreviousPage;
+ },
+ prevPage() {
+ return Number(this.schedules?.pageInfo?.hasPreviousPage);
+ },
+ nextPage() {
+ return Number(this.schedules?.pageInfo?.hasNextPage);
+ },
},
watch: {
// this watcher ensures that the count on the all tab
@@ -245,10 +273,36 @@ export default {
this.reportError(this.$options.i18n.schedulePlayError);
}
},
+ resetPagination() {
+ this.pagination = {
+ ...defaultPagination,
+ };
+ },
fetchPipelineSchedulesByStatus(scope) {
this.scope = scope;
+ this.resetPagination();
this.$apollo.queries.schedules.refetch();
},
+ handlePageChange(page) {
+ const { startCursor, endCursor } = this.schedules.pageInfo;
+
+ if (page > this.pagination.currentPage) {
+ this.pagination = {
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ prevPageCursor: '',
+ nextPageCursor: endCursor,
+ currentPage: page,
+ };
+ } else {
+ this.pagination = {
+ last: SCHEDULES_PER_PAGE,
+ first: null,
+ prevPageCursor: startCursor,
+ currentPage: page,
+ };
+ }
+ },
},
};
</script>
@@ -296,14 +350,25 @@ export default {
<gl-loading-icon v-if="isLoading" size="lg" />
- <pipeline-schedules-table
- v-else
- :schedules="schedules.list"
- :current-user="schedules.currentUser"
- @showTakeOwnershipModal="setTakeOwnershipModal"
- @showDeleteModal="setDeleteModal"
- @playPipelineSchedule="playPipelineSchedule"
- />
+ <template v-else>
+ <pipeline-schedules-table
+ :schedules="schedules.list"
+ :current-user="schedules.currentUser"
+ @showTakeOwnershipModal="setTakeOwnershipModal"
+ @showDeleteModal="setDeleteModal"
+ @playPipelineSchedule="playPipelineSchedule"
+ />
+
+ <gl-pagination
+ v-if="showPagination"
+ :value="pagination.currentPage"
+ :prev-page="prevPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-mt-5"
+ @input="handlePageChange"
+ />
+ </template>
</gl-tab>
<template #tabs-end>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index 0c3ede47015..cd1d9a97ef3 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -370,7 +370,7 @@ export default {
/>
</gl-form-group>
<!--Variable List-->
- <gl-form-group class="gl-mb-2" :label="$options.i18n.variables">
+ <gl-form-group class="gl-mb-0" :label="$options.i18n.variables">
<div
v-for="(variable, index) in variables"
:key="`var-${index}`"
@@ -456,13 +456,23 @@ export default {
<gl-form-checkbox id="schedule-active" v-model="activated" class="gl-mb-3">
{{ $options.i18n.activated }}
</gl-form-checkbox>
-
- <gl-button variant="confirm" data-testid="schedule-submit-button" @click="scheduleHandler">
- {{ buttonText }}
- </gl-button>
- <gl-button :href="schedulesPath" data-testid="schedule-cancel-button">
- {{ $options.i18n.cancel }}
- </gl-button>
+ <div class="gl-display-flex gl-gap-3 gl-flex-wrap">
+ <gl-button
+ variant="confirm"
+ data-testid="schedule-submit-button"
+ class="gl-w-full gl-sm-w-auto"
+ @click="scheduleHandler"
+ >
+ {{ buttonText }}
+ </gl-button>
+ <gl-button
+ :href="schedulesPath"
+ data-testid="schedule-cancel-button"
+ class="gl-w-full gl-sm-w-auto"
+ >
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ </div>
</gl-form>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/constants.js b/app/assets/javascripts/ci/pipeline_schedules/constants.js
index 16dab33ce29..be3feeb6623 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/constants.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/constants.js
@@ -1,3 +1,4 @@
export const VARIABLE_TYPE = 'ENV_VAR';
export const FILE_TYPE = 'FILE';
export const ALL_SCOPE = 'ALL';
+export const SCHEDULES_PER_PAGE = 50;
diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
index 29a26be0344..8fe9fbc5e24 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
@@ -1,7 +1,13 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
query getPipelineSchedulesQuery(
$projectPath: ID!
$status: PipelineScheduleStatus
$ids: [ID!] = null
+ $first: Int
+ $last: Int
+ $prevPageCursor: String = ""
+ $nextPageCursor: String = ""
) {
currentUser {
id
@@ -9,7 +15,14 @@ query getPipelineSchedulesQuery(
}
project(fullPath: $projectPath) {
id
- pipelineSchedules(status: $status, ids: $ids) {
+ pipelineSchedules(
+ status: $status
+ ids: $ids
+ first: $first
+ last: $last
+ after: $nextPageCursor
+ before: $prevPageCursor
+ ) {
count
nodes {
id
@@ -56,6 +69,9 @@ query getPipelineSchedulesQuery(
adminPipelineSchedule
}
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
}
diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
index 6e7d6908cd9..728e8541ae3 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
@@ -47,6 +47,7 @@ export default {
v-else
title=""
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:description="$options.i18n.noCiDescription"
/>
</div>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
index 235126fea0c..0165bbfe69d 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
@@ -7,28 +7,25 @@ export default {
GlButton,
},
props: {
- newPipelinePath: {
+ ciLintPath: {
type: String,
required: false,
default: null,
},
-
- resetCachePath: {
- type: String,
+ isResetCacheButtonLoading: {
+ type: Boolean,
required: false,
- default: null,
+ default: false,
},
-
- ciLintPath: {
+ newPipelinePath: {
type: String,
required: false,
default: null,
},
-
- isResetCacheButtonLoading: {
- type: Boolean,
+ resetCachePath: {
+ type: String,
required: false,
- default: false,
+ default: null,
},
},
methods: {
@@ -61,7 +58,6 @@ export default {
category="primary"
class="js-run-pipeline"
data-testid="run-pipeline-button"
- data-qa-selector="run_pipeline_button"
>
{{ s__('Pipeline|Run pipeline') }}
</gl-button>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
index 082ede60244..8f45094eb74 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
@@ -17,16 +17,15 @@ export default {
targetProjectFullPath: {
default: '',
},
+ pipelineSchedulesPath: {
+ default: '',
+ },
},
props: {
pipeline: {
type: Object,
required: true,
},
- pipelineScheduleUrl: {
- type: String,
- required: true,
- },
},
computed: {
isScheduled() {
@@ -38,6 +37,13 @@ export default {
this.pipeline?.project?.full_path !== `/${this.targetProjectFullPath}`,
);
},
+ showMergedResultsBadge() {
+ // A merge train pipeline is technically also a merged results pipeline,
+ // but we want the badges to be mutually exclusive.
+ return (
+ this.pipeline.flags.merged_result_pipeline && !this.pipeline.flags.merge_train_pipeline
+ );
+ },
autoDevopsTagId() {
return `pipeline-url-autodevops-${this.pipeline.id}`;
},
@@ -52,7 +58,7 @@ export default {
<gl-badge
v-if="isScheduled"
v-gl-tooltip
- :href="pipelineScheduleUrl"
+ :href="pipelineSchedulesPath"
target="__blank"
:title="__('This pipeline was created by a schedule.')"
variant="info"
@@ -74,7 +80,7 @@ export default {
v-gl-tooltip
:title="
s__(
- 'Pipeline|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch.',
+ 'Pipeline|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch.',
)
"
variant="info"
@@ -149,7 +155,7 @@ export default {
v-gl-tooltip
:title="
s__(
- `Pipeline|This pipeline ran on the contents of this merge request's source branch, not the target branch.`,
+ `Pipeline|This pipeline ran on the contents of the merge request's source branch, not the target branch.`,
)
"
variant="info"
@@ -158,6 +164,19 @@ export default {
>{{ s__('Pipeline|merge request') }}</gl-badge
>
<gl-badge
+ v-if="showMergedResultsBadge"
+ v-gl-tooltip
+ :title="
+ s__(
+ `Pipeline|This pipeline ran on the contents of the merge request combined with the contents of the target branch.`,
+ )
+ "
+ variant="info"
+ size="sm"
+ data-testid="pipeline-url-merged-results"
+ >{{ s__('Pipeline|merged results') }}</gl-badge
+ >
+ <gl-badge
v-if="isInFork"
v-gl-tooltip
:title="__('Pipeline ran in fork of project')"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
index b05bdae65c4..8945bb06862 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
@@ -1,22 +1,22 @@
<script>
-import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Tracking from '~/tracking';
import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '~/ci/constants';
-import eventHub from '../../event_hub';
import PipelineMultiActions from './pipeline_multi_actions.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue';
+import PipelineStopModal from './pipeline_stop_modal.vue';
export default {
BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL,
directives: {
GlTooltip: GlTooltipDirective,
- GlModalDirective,
},
components: {
GlButton,
PipelineMultiActions,
PipelinesManualActions,
+ PipelineStopModal,
},
mixins: [Tracking.mixin()],
props: {
@@ -24,15 +24,12 @@ export default {
type: Object,
required: true,
},
- cancelingPipeline: {
- type: Number,
- required: false,
- default: null,
- },
},
data() {
return {
+ isCanceling: false,
isRetrying: false,
+ showConfirmationModal: false,
};
},
computed: {
@@ -41,27 +38,36 @@ export default {
this.pipeline?.details?.has_manual_actions || this.pipeline?.details?.has_scheduled_actions
);
},
- isCancelling() {
- return this.cancelingPipeline === this.pipeline.id;
- },
},
watch: {
pipeline() {
- this.isRetrying = false;
+ if (this.isCanceling || this.isRetrying) {
+ this.isCanceling = false;
+ this.isRetrying = false;
+ }
},
},
methods: {
+ onCloseModal() {
+ this.showConfirmationModal = false;
+ },
+ onConfirmCancelPipeline() {
+ this.isCanceling = true;
+ this.showConfirmationModal = false;
+
+ this.$emit('cancel-pipeline', this.pipeline);
+ },
handleCancelClick() {
+ this.showConfirmationModal = true;
+
this.trackClick('click_cancel_button');
- eventHub.$emit('openConfirmationModal', {
- pipeline: this.pipeline,
- endpoint: this.pipeline.cancel_path,
- });
},
handleRetryClick() {
this.isRetrying = true;
+
this.trackClick('click_retry_button');
- eventHub.$emit('retryPipeline', this.pipeline.retry_path);
+
+ this.$emit('retry-pipeline', this.pipeline);
},
trackClick(action) {
this.track(action, { label: TRACKING_CATEGORIES.table });
@@ -72,8 +78,19 @@ export default {
<template>
<div class="gl-text-right">
+ <pipeline-stop-modal
+ :pipeline="pipeline"
+ :show-confirmation-modal="showConfirmationModal"
+ @submit="onConfirmCancelPipeline"
+ @close-modal="onCloseModal"
+ />
+
<div class="btn-group">
- <pipelines-manual-actions v-if="hasActions" :iid="pipeline.iid" />
+ <pipelines-manual-actions
+ v-if="hasActions"
+ :iid="pipeline.iid"
+ @refresh-pipeline-table="$emit('refresh-pipelines-table')"
+ />
<gl-button
v-if="pipeline.flags.retryable"
@@ -83,7 +100,6 @@ export default {
:disabled="isRetrying"
:loading="isRetrying"
class="js-pipelines-retry-button"
- data-qa-selector="pipeline_retry_button"
data-testid="pipelines-retry-button"
icon="retry"
variant="default"
@@ -94,11 +110,10 @@ export default {
<gl-button
v-if="pipeline.flags.cancelable"
v-gl-tooltip.hover
- v-gl-modal-directive="'confirmation-modal'"
:aria-label="$options.BUTTON_TOOLTIP_CANCEL"
:title="$options.BUTTON_TOOLTIP_CANCEL"
- :loading="isCancelling"
- :disabled="isCancelling"
+ :loading="isCanceling"
+ :disabled="isCanceling"
icon="cancel"
variant="danger"
category="primary"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
index 2da9141df8e..20e2c7e9dce 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
@@ -1,6 +1,5 @@
<script>
import { TRACKING_CATEGORIES } from '~/ci/constants';
-import { CHILD_VIEW } from '~/ci/pipeline_details/constants';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
@@ -16,18 +15,11 @@ export default {
type: Object,
required: true,
},
- viewType: {
- type: String,
- required: true,
- },
},
computed: {
pipelineStatus() {
return this.pipeline?.details?.status ?? {};
},
- isChildView() {
- return this.viewType === CHILD_VIEW;
- },
},
methods: {
trackClick() {
@@ -39,13 +31,7 @@ export default {
<template>
<div>
- <ci-badge-link
- class="gl-mb-3"
- :status="pipelineStatus"
- :show-text="!isChildView"
- data-qa-selector="pipeline_commit_status"
- @ciStatusBadgeClick="trackClick"
- />
+ <ci-badge-link class="gl-mb-3" :status="pipelineStatus" @ciStatusBadgeClick="trackClick" />
<pipelines-timeago :pipeline="pipeline" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
index 9f38be668f2..d62a68f0dcc 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
@@ -7,7 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
/**
* Pipeline Stop Modal.
*
- * Renders the modal used to confirm stopping a pipeline.
+ * Renders the modal used to confirm cancelling a pipeline.
*/
export default {
components: {
@@ -22,8 +22,15 @@ export default {
required: true,
deep: true,
},
+ showConfirmationModal: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
+ hasRef() {
+ return !isEmpty(this.pipeline.ref);
+ },
modalTitle() {
return sprintf(
s__('Pipeline|Stop pipeline #%{pipelineId}?'),
@@ -34,10 +41,7 @@ export default {
);
},
modalText() {
- return s__(`Pipeline|You’re about to stop pipeline #%{pipelineId}.`);
- },
- hasRef() {
- return !isEmpty(this.pipeline.ref);
+ return s__(`Pipeline|You're about to stop pipeline #%{pipelineId}.`);
},
primaryProps() {
return {
@@ -45,10 +49,13 @@ export default {
attributes: { variant: 'danger' },
};
},
- cancelProps() {
- return {
- text: __('Cancel'),
- };
+ showModal: {
+ get() {
+ return this.showConfirmationModal;
+ },
+ set() {
+ this.$emit('close-modal');
+ },
},
},
methods: {
@@ -56,14 +63,16 @@ export default {
this.$emit('submit', event);
},
},
+ cancelProps: { text: __('Cancel') },
};
</script>
<template>
<gl-modal
+ v-model="showModal"
modal-id="confirmation-modal"
:title="modalTitle"
:action-primary="primaryProps"
- :action-cancel="cancelProps"
+ :action-cancel="$options.cancelProps"
@primary="emitSubmit($event)"
>
<p>
@@ -74,7 +83,7 @@ export default {
</gl-sprintf>
</p>
- <p v-if="pipeline">
+ <p>
<ci-icon
v-if="pipeline.details"
:status="pipeline.details.status"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
index edaeb481d7b..9a49eefbf98 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
import Tracking from '~/tracking';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { ICONS, TRACKING_CATEGORIES } from '~/ci/constants';
+import { ICONS, PIPELINE_ID_KEY, PIPELINE_IID_KEY, TRACKING_CATEGORIES } from '~/ci/constants';
import PipelineLabels from './pipeline_labels.vue';
export default {
@@ -24,13 +24,13 @@ export default {
type: Object,
required: true,
},
- pipelineScheduleUrl: {
+ pipelineIdType: {
type: String,
- required: true,
- },
- pipelineKey: {
- type: String,
- required: true,
+ required: false,
+ default: PIPELINE_ID_KEY,
+ validator(value) {
+ return value === PIPELINE_IID_KEY || value === PIPELINE_ID_KEY;
+ },
},
refClass: {
type: String,
@@ -173,9 +173,8 @@ export default {
:href="pipeline.path"
class="gl-mr-1 gl-text-blue-500!"
data-testid="pipeline-url-link"
- data-qa-selector="pipeline_url_link"
@click="trackClick('click_pipeline_id')"
- >#{{ pipeline[pipelineKey] }}</gl-link
+ >#{{ pipeline[pipelineIdType] }}</gl-link
>
<!--Commit row-->
<div class="gl-display-inline-flex gl-rounded-base gl-px-2 gl-bg-gray-50 gl-text-gray-700">
@@ -237,6 +236,6 @@ export default {
/>
<!--End of commit row-->
</div>
- <pipeline-labels :pipeline-schedule-url="pipelineScheduleUrl" :pipeline="pipeline" />
+ <pipeline-labels :pipeline="pipeline" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
index 4dacd474bde..ebf1744aee2 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
@@ -6,7 +6,6 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m
import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-import eventHub from '../../event_hub';
import { TRACKING_CATEGORIES } from '../../constants';
import getPipelineActionsQuery from '../graphql/queries/get_pipeline_actions.query.graphql';
@@ -94,7 +93,7 @@ export default {
.post(`${action.playPath}.json`)
.then(() => {
this.isLoading = false;
- eventHub.$emit('updateTable');
+ this.$emit('refresh-pipeline-table');
})
.catch(() => {
this.isLoading = false;
diff --git a/app/assets/javascripts/ci/pipelines_page/pipelines.vue b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
index 87ee5463bb0..faa013079be 100644
--- a/app/assets/javascripts/ci/pipelines_page/pipelines.vue
+++ b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
@@ -1,5 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
+import NO_PIPELINES_SVG from '@gitlab/svgs/dist/illustrations/empty-state/empty-pipeline-md.svg?url';
+import ERROR_STATE_SVG from '@gitlab/svgs/dist/illustrations/pipelines_failed.svg?url';
import { GlEmptyState, GlIcon, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
import { isEqual } from 'lodash';
import * as Sentry from '@sentry/browser';
@@ -9,11 +11,12 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import {
FILTER_TAG_IDENTIFIER,
- PipelineKeyOptions,
+ PIPELINE_ID_KEY,
+ PIPELINE_IID_KEY,
RAW_TEXT_WARNING,
TRACKING_CATEGORIES,
} from '~/ci/constants';
-import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
+import PipelinesTable from '~/ci/common/pipelines_table.vue';
import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
import { validateParams } from '~/ci/pipeline_details/utils';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
@@ -27,7 +30,6 @@ import NavigationControls from './components/nav_controls.vue';
import PipelinesFilteredSearch from './components/pipelines_filtered_search.vue';
export default {
- PipelineKeyOptions,
components: {
NoCiEmptyState,
GlCollapsibleListbox,
@@ -37,7 +39,7 @@ export default {
NavigationTabs,
NavigationControls,
PipelinesFilteredSearch,
- PipelinesTableComponent,
+ PipelinesTable,
TablePagination,
},
mixins: [PipelinesMixin, Tracking.mixin()],
@@ -46,36 +48,10 @@ export default {
type: Object,
required: true,
},
- // Can be rendered in 3 different places, with some visual differences
- // Accepts root | child
- // `root` -> main view
- // `child` -> rendered inside MR or Commit View
- viewType: {
- type: String,
- required: false,
- default: 'root',
- },
endpoint: {
type: String,
required: true,
},
- pipelineScheduleUrl: {
- type: String,
- required: false,
- default: '',
- },
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- errorStateSvgPath: {
- type: String,
- required: true,
- },
- noPipelinesSvgPath: {
- type: String,
- required: true,
- },
hasGitlabCi: {
type: Boolean,
required: true,
@@ -243,8 +219,9 @@ export default {
},
selectedPipelineKeyOption() {
return (
- this.$options.PipelineKeyOptions.find((e) => this.visibilityPipelineIdType === e.value) ||
- this.$options.PipelineKeyOptions[0]
+ this.$options.pipelineKeyOptions.find(
+ (option) => this.visibilityPipelineIdType === option.value,
+ ) || this.$options.pipelineKeyOptions[0]
);
},
},
@@ -334,11 +311,12 @@ export default {
},
changeVisibilityPipelineIDType(idType) {
this.visibilityPipelineIdType = idType;
- this.saveVisibilityPipelineIDType(idType);
+
+ if (isLoggedIn()) {
+ this.saveVisibilityPipelineIDType(idType);
+ }
},
saveVisibilityPipelineIDType(idType) {
- if (!isLoggedIn()) return;
-
this.$apollo
.mutate({
mutation: setSortPreferenceMutation,
@@ -354,6 +332,20 @@ export default {
});
},
},
+ errorStateSvgPath: ERROR_STATE_SVG,
+ noPipelinesSvgPath: NO_PIPELINES_SVG,
+ pipelineKeyOptions: [
+ {
+ text: __('Show Pipeline ID'),
+ label: __('Pipeline ID'),
+ value: PIPELINE_ID_KEY,
+ },
+ {
+ text: __('Show Pipeline IID'),
+ label: __('Pipeline IID'),
+ value: PIPELINE_IID_KEY,
+ },
+ ],
};
</script>
<template>
@@ -393,9 +385,8 @@ export default {
/>
<gl-collapsible-listbox
v-model="visibilityPipelineIdType"
- data-testid="pipeline-key-collapsible-box"
:toggle-text="selectedPipelineKeyOption.text"
- :items="$options.PipelineKeyOptions"
+ :items="$options.pipelineKeyOptions"
@select="changeVisibilityPipelineIDType"
/>
</div>
@@ -411,32 +402,34 @@ export default {
<no-ci-empty-state
v-else-if="stateToRender === $options.stateMap.emptyState"
- :empty-state-svg-path="emptyStateSvgPath"
+ :empty-state-svg-path="$options.noPipelinesSvgPath"
:can-set-ci="canCreatePipeline"
:registration-token="registrationToken"
/>
<gl-empty-state
v-else-if="stateToRender === $options.stateMap.error"
- :svg-path="errorStateSvgPath"
+ :svg-path="$options.errorStateSvgPath"
+ :svg-height="null"
:title="s__('Pipelines|There was an error fetching the pipelines.')"
:description="s__('Pipelines|Try again in a few moments or contact your support team.')"
/>
<gl-empty-state
v-else-if="stateToRender === $options.stateMap.emptyTab"
- :svg-path="noPipelinesSvgPath"
+ :svg-path="$options.noPipelinesSvgPath"
:svg-height="150"
:title="emptyTabMessage"
/>
<div v-else-if="stateToRender === $options.stateMap.tableList">
- <pipelines-table-component
+ <pipelines-table
:pipelines="state.pipelines"
- :pipeline-schedule-url="pipelineScheduleUrl"
:update-graph-dropdown="updateGraphDropdown"
- :view-type="viewType"
- :pipeline-key-option="selectedPipelineKeyOption"
+ :pipeline-id-type="selectedPipelineKeyOption.value"
+ @cancel-pipeline="onCancelPipeline"
+ @refresh-pipelines-table="onRefreshPipelinesTable"
+ @retry-pipeline="onRetryPipeline"
/>
</div>
diff --git a/app/assets/javascripts/ci/runner/components/registration/utils.js b/app/assets/javascripts/ci/runner/components/registration/utils.js
index c8a75506c9c..c1885be9585 100644
--- a/app/assets/javascripts/ci/runner/components/registration/utils.js
+++ b/app/assets/javascripts/ci/runner/components/registration/utils.js
@@ -3,8 +3,8 @@ import {
LINUX_PLATFORM,
MACOS_PLATFORM,
WINDOWS_PLATFORM,
- DOWNLOAD_LOCATIONS,
-} from '../../constants';
+ RUNNER_PACKAGE_HOST,
+} from 'jh_else_ce/ci/runner/constants';
import linuxInstall from './scripts/linux/install.sh?raw';
import osxInstall from './scripts/osx/install.sh?raw';
import windowsInstall from './scripts/windows/install.ps1?raw';
@@ -27,6 +27,47 @@ const OS = {
},
};
+export const DOWNLOAD_LOCATIONS = {
+ [LINUX_PLATFORM]: [
+ {
+ arch: 'amd64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-amd64`,
+ },
+ {
+ arch: '386',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-386`,
+ },
+ {
+ arch: 'arm',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-arm`,
+ },
+ {
+ arch: 'arm64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-arm64`,
+ },
+ ],
+ [MACOS_PLATFORM]: [
+ {
+ arch: 'amd64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-darwin-amd64`,
+ },
+ {
+ arch: 'arm64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-darwin-arm64`,
+ },
+ ],
+ [WINDOWS_PLATFORM]: [
+ {
+ arch: 'amd64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-windows-amd64.exe`,
+ },
+ {
+ arch: '386',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-windows-386.exe`,
+ },
+ ],
+};
+
export const commandPrompt = ({ platform }) => {
return (OS[platform] || OS[DEFAULT_PLATFORM]).commandPrompt;
};
diff --git a/app/assets/javascripts/ci/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue
index fac90fb0370..0ec2ef30c20 100644
--- a/app/assets/javascripts/ci/runner/components/runner_details.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_details.vue
@@ -29,10 +29,6 @@ export default {
import('ee_component/ci/runner/components/runner_maintenance_note_detail.vue'),
RunnerGroups,
RunnerProjects,
- RunnerUpgradeStatusBadge: () =>
- import('ee_component/ci/runner/components/runner_upgrade_status_badge.vue'),
- RunnerUpgradeStatusAlert: () =>
- import('ee_component/ci/runner/components/runner_upgrade_status_alert.vue'),
RunnerTags,
RunnerManagersDetail,
TimeAgo,
@@ -92,7 +88,6 @@ export default {
<template>
<div>
- <runner-upgrade-status-alert class="gl-my-4" :runner="runner" />
<div class="gl-pt-4">
<dl class="gl-mb-0 gl-display-grid runner-details-grid-template">
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
@@ -104,16 +99,6 @@ export default {
<time-ago :time="runner.contactedAt" />
</template>
</runner-detail>
- <runner-detail :label="s__('Runners|Version')">
- <template v-if="runner.version" #value>
- {{ runner.version }}
- <runner-upgrade-status-badge size="sm" :runner="runner" />
- </template>
- </runner-detail>
- <runner-detail :label="s__('Runners|IP Address')" :value="runner.ipAddress" />
- <runner-detail :label="s__('Runners|Executor')" :value="runner.executorName" />
- <runner-detail :label="s__('Runners|Architecture')" :value="runner.architectureName" />
- <runner-detail :label="s__('Runners|Platform')" :value="runner.platformName" />
<runner-detail :label="s__('Runners|Configuration')">
<template v-if="configTextProtected || configTextUntagged" #value>
<gl-intersperse>
diff --git a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
index 38e36733045..b8c80986fbc 100644
--- a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
@@ -92,9 +92,7 @@ export default {
<gl-form-group :label="__('Tags')" label-for="runner-tags">
<template #description>
<gl-sprintf
- :message="
- s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.')
- "
+ :message="s__('Runners|Separate multiple tags with a comma. For example, %{example}.')"
>
<template #example>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
@@ -106,7 +104,7 @@ export default {
<gl-sprintf
:message="
s__(
- 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}',
+ 'Runners|Add tags to specify jobs that the runner can run. %{helpLinkStart}Learn more.%{helpLinkEnd}',
)
"
>
@@ -191,7 +189,9 @@ export default {
)
"
label-for="runner-max-timeout"
- :description="s__('Runners|Enter the number of seconds.')"
+ :description="
+ s__('Runners|Enter the job timeout in seconds. Must be a minimum of 600 seconds.')
+ "
>
<gl-form-input
id="runner-max-timeout"
diff --git a/app/assets/javascripts/ci/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue
index 55a33ef2074..0fa06537ed6 100644
--- a/app/assets/javascripts/ci/runner/components/runner_header.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_header.vue
@@ -13,6 +13,8 @@ export default {
TimeAgo,
RunnerTypeBadge,
RunnerStatusBadge,
+ RunnerUpgradeStatusBadge: () =>
+ import('ee_component/ci/runner/components/runner_upgrade_status_badge.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -40,6 +42,7 @@ export default {
<div class="gl-display-flex gl-align-items-flex-start gl-gap-3 gl-flex-wrap gl-mt-3">
<runner-status-badge :contacted-at="runner.contactedAt" :status="runner.status" />
<runner-type-badge :type="runner.runnerType" />
+ <runner-upgrade-status-badge :runner="runner" />
<span v-if="runner.createdAt">
<gl-sprintf :message="__('%{locked} created %{timeago}')">
<template #locked>
diff --git a/app/assets/javascripts/ci/runner/components/runner_type_icon.vue b/app/assets/javascripts/ci/runner/components/runner_type_icon.vue
new file mode 100644
index 00000000000..c56f28e10a3
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_type_icon.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_INSTANCE_TYPE,
+ I18N_INSTANCE_RUNNER_DESCRIPTION,
+ I18N_GROUP_TYPE,
+ I18N_GROUP_RUNNER_DESCRIPTION,
+ I18N_PROJECT_TYPE,
+ I18N_PROJECT_RUNNER_DESCRIPTION,
+} from '../constants';
+
+const ICON_DATA = {
+ [INSTANCE_TYPE]: {
+ name: 'users',
+ tooltip: `${I18N_INSTANCE_TYPE}: ${I18N_INSTANCE_RUNNER_DESCRIPTION}`,
+ },
+ [GROUP_TYPE]: {
+ name: 'group',
+ tooltip: `${I18N_GROUP_TYPE}: ${I18N_GROUP_RUNNER_DESCRIPTION}`,
+ },
+ [PROJECT_TYPE]: {
+ name: 'project',
+ tooltip: `${I18N_PROJECT_TYPE}: ${I18N_PROJECT_RUNNER_DESCRIPTION}`,
+ },
+};
+
+export default {
+ components: {
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ type: {
+ type: String,
+ required: false,
+ default: null,
+ validator(type) {
+ return Boolean(ICON_DATA[type]);
+ },
+ },
+ },
+ computed: {
+ icon() {
+ return ICON_DATA[this.type];
+ },
+ },
+};
+</script>
+<template>
+ <gl-icon
+ v-if="icon"
+ v-gl-tooltip="icon.tooltip"
+ :aria-label="icon.tooltip"
+ :name="icon.name"
+ v-bind="$attrs"
+ />
+</template>
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index 3293c68ddb8..b3cc295f8e4 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -216,54 +216,8 @@ export const LINUX_PLATFORM = 'linux';
export const MACOS_PLATFORM = 'osx';
export const WINDOWS_PLATFORM = 'windows';
-export const DOWNLOAD_LOCATIONS = {
- [LINUX_PLATFORM]: [
- {
- arch: 'amd64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64',
- },
- {
- arch: '386',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386',
- },
- {
- arch: 'arm',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm',
- },
- {
- arch: 'arm64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64',
- },
- ],
- [MACOS_PLATFORM]: [
- {
- arch: 'amd64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64',
- },
- {
- arch: 'arm64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64',
- },
- ],
- [WINDOWS_PLATFORM]: [
- {
- arch: 'amd64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe',
- },
- {
- arch: '386',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe',
- },
- ],
-};
+// About Gitlab Runner Package host
+export const RUNNER_PACKAGE_HOST = 'gitlab-runner-downloads.s3.amazonaws.com';
export const DEFAULT_PLATFORM = LINUX_PLATFORM;
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
index 1a2ad59650e..e2c890b3834 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
@@ -6,10 +6,6 @@ fragment RunnerDetailsShared on CiRunner {
accessLevel
runUntagged
locked
- ipAddress
- executorName
- architectureName
- platformName
description
maximumTimeout
jobCount
diff --git a/app/assets/javascripts/ci/runner/sentry_utils.js b/app/assets/javascripts/ci/runner/sentry_utils.js
index 29de1f9adae..25fecdcfa7d 100644
--- a/app/assets/javascripts/ci/runner/sentry_utils.js
+++ b/app/assets/javascripts/ci/runner/sentry_utils.js
@@ -6,15 +6,16 @@ const COMPONENT_TAG = 'vue_component';
* Captures an error in a Vue component and sends it
* to Sentry
*
- * @param {Object} options
- * @param {Error} options.error - Exception or error
- * @param {String} options.component - Component name in CamelCase format
+ * @param {Object} options Exception details
+ * @param {Object} options.error An exception-like object
+ * @param {string} [options.component=] Component name in CamelCase format
*/
export const captureException = ({ error, component }) => {
- Sentry.withScope((scope) => {
- if (component) {
- scope.setTag(COMPONENT_TAG, component);
- }
+ if (component) {
+ Sentry.captureException(error, {
+ tags: { [COMPONENT_TAG]: component },
+ });
+ } else {
Sentry.captureException(error);
- });
+ }
};
diff --git a/app/assets/javascripts/ci/utils.js b/app/assets/javascripts/ci/utils.js
index eb9e9538b75..8a4f28404c6 100644
--- a/app/assets/javascripts/ci/utils.js
+++ b/app/assets/javascripts/ci/utils.js
@@ -1,17 +1,9 @@
import * as Sentry from '@sentry/browser';
export const reportToSentry = (component, failureType) => {
- Sentry.withScope((scope) => {
- scope.setTag('component', component);
- Sentry.captureException(failureType);
- });
-};
-
-export const reportMessageToSentry = (component, message, context) => {
- Sentry.withScope((scope) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- scope.setContext('Vue data', context);
- scope.setTag('component', component);
- Sentry.captureMessage(message);
+ Sentry.captureException(failureType, {
+ tags: {
+ component,
+ },
});
};
diff --git a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
index 2f45ef8a862..4a20f9ec10d 100644
--- a/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_empty_state.vue
@@ -16,7 +16,11 @@ export default {
</script>
<template>
- <gl-empty-state :svg-path="emptyStateImage" :svg-height="100">
+ <gl-empty-state
+ :svg-path="emptyStateImage"
+ :svg-height="100"
+ data-testid="cluster-agent-empty-state"
+ >
<template #title>
<gl-sprintf :message="$options.i18n.introText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
index f4134ab5072..339ea3b7c0d 100644
--- a/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters_empty_state.vue
@@ -22,7 +22,11 @@ export default {
<template>
<div>
- <gl-empty-state :svg-path="clustersEmptyStateImage" :svg-height="100">
+ <gl-empty-state
+ :svg-path="clustersEmptyStateImage"
+ :svg-height="100"
+ data-testid="clusters-empty-state"
+ >
<template #title>
<p>
<gl-sprintf :message="$options.i18n.introText">
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 1ea18dcc97d..4537fd51fcf 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -17,9 +17,10 @@ const allNodesPresent = (clusters, retryCount) => {
};
export const reportSentryError = (_store, { error, tag }) => {
- Sentry.withScope((scope) => {
- scope.setTag('javascript_clusters_list', tag);
- Sentry.captureException(error);
+ Sentry.captureException(error, {
+ tags: {
+ javascript_clusters_list: tag,
+ },
});
};
diff --git a/app/assets/javascripts/comment_templates/components/form.vue b/app/assets/javascripts/comment_templates/components/form.vue
index c29482eab7a..5a5d221591a 100644
--- a/app/assets/javascripts/comment_templates/components/form.vue
+++ b/app/assets/javascripts/comment_templates/components/form.vue
@@ -93,7 +93,7 @@ export default {
this.$emit('saved');
this.updateCommentTemplate = { name: '', content: '' };
this.showValidation = false;
- this.track_event('i_code_review_saved_replies_create');
+ this.trackEvent('i_code_review_saved_replies_create');
}
},
})
diff --git a/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue b/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
index 5e84dcbe48e..1954f9f8f35 100644
--- a/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
+++ b/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
@@ -2,8 +2,8 @@
import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getParameterByName } from '~/lib/utils/url_utility';
-import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
-import { PipelineKeyOptions } from '~/ci/constants';
+import PipelinesTable from '~/ci/common/pipelines_table.vue';
+import { PIPELINE_ID_KEY } from '~/ci/constants';
import eventHub from '~/ci/event_hub';
import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
import PipelinesService from '~/ci/pipelines_page/services/pipelines_service';
@@ -13,7 +13,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__, __ } from '~/locale';
export default {
- PipelineKeyOptions,
components: {
GlButton,
GlEmptyState,
@@ -21,7 +20,7 @@ export default {
GlLoadingIcon,
GlModal,
GlSprintf,
- PipelinesTableComponent,
+ PipelinesTable,
TablePagination,
},
mixins: [PipelinesMixin, glFeatureFlagMixin()],
@@ -180,6 +179,7 @@ export default {
}
},
},
+ pipelineIdKey: PIPELINE_ID_KEY,
modal: {
actionPrimary: {
text: s__('Pipeline|Run pipeline'),
@@ -225,6 +225,7 @@ export default {
<gl-empty-state
v-else-if="shouldRenderErrorState"
:svg-path="errorStateSvgPath"
+ :svg-height="null"
:title="
s__(`Pipelines|There was an error fetching the pipelines.
Try again in a few moments or contact your support team.`)
@@ -279,11 +280,14 @@ export default {
{{ $options.i18n.runPipelineText }}
</gl-button>
- <pipelines-table-component
+ <pipelines-table
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:view-type="viewType"
- :pipeline-key-option="$options.PipelineKeyOptions[0]"
+ :pipeline-id-type="$options.pipelineIdKey"
+ @cancel-pipeline="onCancelPipeline"
+ @refresh-pipelines-table="onRefreshPipelinesTable"
+ @retry-pipeline="onRetryPipeline"
>
<template #table-header-actions>
<div v-if="canRenderPipelineButton" class="gl-text-right">
@@ -296,7 +300,7 @@ export default {
</gl-button>
</div>
</template>
- </pipelines-table-component>
+ </pipelines-table>
</div>
<gl-modal
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index beeb9b9ada4..6ca59f634a2 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -11,7 +11,7 @@ const apolloProvider = new VueApollo({
/**
* Used in:
- * - Project Pipelines List (projects:pipelines:index)
+ * - Project Pipelines List (projects:pipelines)
* - Commit details View > Pipelines Tab > Pipelines Table (projects:commit:pipelines)
* - 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)
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 25c03496a76..2e9388c1e20 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -4,6 +4,8 @@ import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { __ } from '~/locale';
import { VARIANT_DANGER } from '~/alert';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+import { CONTENT_EDITOR_READY_EVENT } from '~/vue_shared/constants';
+import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import { createContentEditor } from '../services/create_content_editor';
import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants';
import ContentEditorAlert from './content_editor_alert.vue';
@@ -161,9 +163,10 @@ export default {
},
});
},
- mounted() {
+ async mounted() {
this.$emit('initialized');
- this.setSerializedContent(this.markdown);
+ await this.setSerializedContent(this.markdown);
+ markdownEditorEventHub.$emit(CONTENT_EDITOR_READY_EVENT);
},
beforeDestroy() {
this.contentEditor.dispose();
@@ -238,11 +241,7 @@ export default {
@keydown="$emit('keydown', $event)"
/>
<content-editor-alert />
- <div
- data-testid="content-editor"
- data-qa-selector="content_editor_container"
- :class="{ 'is-focused': focused }"
- >
+ <div data-testid="content-editor" :class="{ 'is-focused': focused }">
<formatting-toolbar
ref="toolbar"
:supports-quick-actions="supportsQuickActions"
@@ -275,7 +274,8 @@ export default {
target="_blank"
category="tertiary"
size="small"
- title="Markdown is supported"
+ :title="__('Markdown is supported')"
+ :aria-label="__('Markdown is supported')"
class="gl-px-3!"
/>
</div>
diff --git a/app/assets/javascripts/content_editor/components/content_editor_provider.vue b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
index fa842f23cc3..955fa129ce7 100644
--- a/app/assets/javascripts/content_editor/components/content_editor_provider.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
@@ -1,13 +1,7 @@
<script>
export default {
provide() {
- // We can't use this.contentEditor due to bug in vue-apollo when
- // provide is called in beforeCreate
- // See https://github.com/vuejs/vue-apollo/pull/1153 for details
-
- // @vue-compat does not care to normalize propsData fields
- const contentEditor =
- this.$options.propsData.contentEditor || this.$options.propsData['content-editor'];
+ const { contentEditor } = this;
return {
contentEditor,
diff --git a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
index 4cf150dd948..78a01693f14 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
@@ -58,7 +58,7 @@ export default {
name="content_editor_image"
class="gl-display-none"
:aria-label="$options.i18n.inputLabel"
- data-qa-selector="file_upload_field"
+ data-testid="file-upload-field"
@change="onFileSelect"
/>
</span>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
index bd30bdcea0c..4b1e14665de 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
@@ -75,7 +75,7 @@ export default {
:selected="activeItemLabel"
:disabled="!activeItem"
:data-qa-text-style="activeItemLabel"
- data-qa-selector="text_style_dropdown"
+ data-testid="text-style-dropdown"
size="small"
toggle-class="btn-default-tertiary"
@select="execute"
diff --git a/app/assets/javascripts/content_editor/extensions/selection.js b/app/assets/javascripts/content_editor/extensions/selection.js
index 2e0bb29e5a1..0c24207b395 100644
--- a/app/assets/javascripts/content_editor/extensions/selection.js
+++ b/app/assets/javascripts/content_editor/extensions/selection.js
@@ -6,12 +6,22 @@ export default Extension.create({
name: 'selection',
addProseMirrorPlugins() {
+ let contextMenuVisible = false;
+
return [
new Plugin({
key: new PluginKey('selection'),
props: {
+ handleDOMEvents: {
+ contextmenu() {
+ contextMenuVisible = true;
+ setTimeout(() => {
+ contextMenuVisible = false;
+ });
+ },
+ },
decorations(state) {
- if (state.selection.empty) return null;
+ if (state.selection.empty || contextMenuVisible) return null;
return DecorationSet.create(state.doc, [
Decoration.inline(state.selection.from, state.selection.to, {
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 17e650644b3..0897232cf89 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -561,7 +561,14 @@ const linkType = (sourceMarkdown) => {
return LINK_HTML;
};
-const normalizeUrl = (url) => decodeURIComponent(removeLastSlashInUrlPath(removeUrlProtocol(url)));
+const normalizeUrl = (url) => {
+ const processedUrl = removeLastSlashInUrlPath(removeUrlProtocol(url));
+ try {
+ return decodeURIComponent(processedUrl);
+ } catch {
+ return processedUrl;
+ }
+};
/**
* Validates that the provided URL is a valid GFM autolink
diff --git a/app/assets/javascripts/crm/organizations/components/graphql/create_organization.mutation.graphql b/app/assets/javascripts/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql
index 2cc7e53ee9b..8e019420eb7 100644
--- a/app/assets/javascripts/crm/organizations/components/graphql/create_organization.mutation.graphql
+++ b/app/assets/javascripts/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql
@@ -1,6 +1,6 @@
#import "./crm_organization_fields.fragment.graphql"
-mutation createOrganization($input: CustomerRelationsOrganizationCreateInput!) {
+mutation createCustomerRelationsOrganization($input: CustomerRelationsOrganizationCreateInput!) {
customerRelationsOrganizationCreate(input: $input) {
organization {
...OrganizationFragment
diff --git a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
index 4d2a038458d..fb056e4fa2c 100644
--- a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
+++ b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
@@ -4,7 +4,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_CRM_ORGANIZATION, TYPENAME_GROUP } from '~/graphql_shared/constants';
import CrmForm from '../../components/crm_form.vue';
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
-import createOrganizationMutation from './graphql/create_organization.mutation.graphql';
+import createCustomerRelationsOrganizationMutation from './graphql/create_customer_relations_organization.mutation.graphql';
import updateOrganizationMutation from './graphql/update_organization.mutation.graphql';
export default {
@@ -31,7 +31,7 @@ export default {
mutation() {
if (this.isEditMode) return updateOrganizationMutation;
- return createOrganizationMutation;
+ return createCustomerRelationsOrganizationMutation;
},
getQuery() {
return {
diff --git a/app/assets/javascripts/custom_emoji/components/app.vue b/app/assets/javascripts/custom_emoji/components/app.vue
index 405a296397f..00b904fbea4 100644
--- a/app/assets/javascripts/custom_emoji/components/app.vue
+++ b/app/assets/javascripts/custom_emoji/components/app.vue
@@ -8,7 +8,7 @@ export default {};
<h4 class="gl-mt-0">
{{ __('Custom emoji') }}
</h4>
- <p>{{ __('Custom emoji will be available to use in every project in group.') }}</p>
+ <p>{{ __('Custom emoji will be available to use in every project in the group.') }}</p>
<router-view />
</div>
</div>
diff --git a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
index ccf4b064fa4..f21086185fb 100644
--- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -283,7 +283,7 @@ export default {
</template>
</gl-sprintf>
</template>
- <gl-form-input id="deploy_token_username" v-model="username" class="gl-form-input-xl" />
+ <gl-form-input id="deploy_token_username" v-model="username" width="xl" />
</gl-form-group>
<gl-form-group
:label="$options.translations.addTokenScopesLabel"
diff --git a/app/assets/javascripts/design_management/components/design_description/description_form.vue b/app/assets/javascripts/design_management/components/design_description/description_form.vue
index 413442074f0..6be643e88dc 100644
--- a/app/assets/javascripts/design_management/components/design_description/description_form.vue
+++ b/app/assets/javascripts/design_management/components/design_description/description_form.vue
@@ -4,7 +4,6 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { toggleMarkCheckboxes } from '~/behaviors/markdown/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -36,7 +35,6 @@ export default {
placeholder: s__('DesignManagement|Write a comment or drag your files here…'),
'aria-label': s__('DesignManagement|Design description'),
},
- mixins: [glFeaturesFlagMixin()],
markdownDocsPath: helpPagePath('user/markdown'),
quickActionsDocsPath: helpPagePath('user/project/quick_actions'),
props: {
@@ -174,7 +172,6 @@ export default {
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="$options.markdownDocsPath"
:form-field-props="$options.formFieldProps"
- :enable-content-editor="Boolean(glFeatures.contentEditorOnIssues)"
:quick-actions-docs-path="$options.quickActionsDocsPath"
:autosave-key="autosaveKey"
enable-autocomplete
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 08306312c2e..924c515ee2d 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlPagination, GlSprintf, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters, mapActions } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import api from '~/api';
import {
@@ -22,6 +23,7 @@ import { __ } from '~/locale';
import notesEventHub from '~/notes/event_hub';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
+import { sortFindingsByFile } from '../utils/sort_findings_by_file';
import {
MR_TREE_SHOW_KEY,
ALERT_OVERFLOW_HIDDEN,
@@ -42,7 +44,7 @@ import {
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
-import { updateChangesTabCount } from '../utils/merge_request';
+import { updateChangesTabCount, extractFileHash } from '../utils/merge_request';
import { queueRedisHllEvents } from '../utils/queue_events';
import FindingsDrawer from './shared/findings_drawer.vue';
import CollapsedFilesWarning from './collapsed_files_warning.vue';
@@ -53,6 +55,7 @@ import HiddenFilesWarning from './hidden_files_warning.vue';
import NoChanges from './no_changes.vue';
import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync';
import DiffsFileTree from './diffs_file_tree.vue';
+import getMRCodequalityReports from './graphql/get_mr_codequality_reports.query.graphql';
export default {
name: 'DiffsApp',
@@ -75,6 +78,7 @@ export default {
GenerateTestFileDrawer: () =>
import('ee_component/ai/components/generate_test_file_drawer.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
alerts: {
ALERT_OVERFLOW_HIDDEN,
ALERT_MERGE_CONFLICT,
@@ -86,6 +90,16 @@ export default {
required: false,
default: '',
},
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ iid: {
+ type: String,
+ required: false,
+ default: '',
+ },
endpointSast: {
type: String,
required: false,
@@ -123,6 +137,32 @@ export default {
subscribedToVirtualScrollingEvents: false,
};
},
+ apollo: {
+ getMRCodequalityReports: {
+ query: getMRCodequalityReports,
+ variables() {
+ return { fullPath: this.projectPath, iid: this.iid };
+ },
+ skip() {
+ return !this.endpointCodequality || !this.sastReportsInInlineDiff;
+ },
+ update(data) {
+ if (data?.project?.mergeRequest?.codequalityReportsComparer?.report?.newErrors) {
+ this.$store.commit(
+ 'diffs/SET_CODEQUALITY_DATA',
+ sortFindingsByFile(
+ data.project.mergeRequest.codequalityReportsComparer.report.newErrors,
+ ),
+ );
+ }
+ },
+ error() {
+ createAlert({
+ message: __('Something went wrong fetching the CodeQuality Findings. Please try again!'),
+ });
+ },
+ },
+ },
computed: {
...mapState('diffs', {
numTotalFiles: 'realSize',
@@ -220,6 +260,9 @@ export default {
resourceId() {
return convertToGraphQLId('MergeRequest', this.getNoteableData.id);
},
+ sastReportsInInlineDiff() {
+ return this.glFeatures.sastReportsInInlineDiff;
+ },
},
watch: {
commit(newCommit, oldCommit) {
@@ -344,12 +387,13 @@ export default {
...mapActions(['startTaskList']),
...mapActions('diffs', [
'moveToNeighboringCommit',
- 'setBaseConfig',
'setCodequalityEndpoint',
'setSastEndpoint',
'fetchDiffFilesMeta',
'fetchDiffFilesBatch',
'fetchFileByFile',
+ 'loadCollapsedDiff',
+ 'setFileForcedOpen',
'fetchCoverageFiles',
'fetchCodequality',
'fetchSast',
@@ -373,15 +417,34 @@ export default {
notesEventHub.$on('refetchDiffData', this.refetchDiffData);
notesEventHub.$on('fetchedNotesData', this.rereadNoteHash);
diffsEventHub.$on('diffFilesModified', this.setDiscussions);
+ diffsEventHub.$on('doneLoadingBatches', this.autoScroll);
diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData);
},
unsubscribeFromEvents() {
diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData);
+ diffsEventHub.$off('doneLoadingBatches', this.autoScroll);
diffsEventHub.$off('diffFilesModified', this.setDiscussions);
notesEventHub.$off('fetchedNotesData', this.rereadNoteHash);
notesEventHub.$off('refetchDiffData', this.refetchDiffData);
notesEventHub.$off('fetchDiffData', this.fetchData);
},
+ autoScroll() {
+ const lineCode = window.location.hash;
+ const sha1InHash = extractFileHash({ input: lineCode });
+
+ if (sha1InHash) {
+ const idx = this.diffs.findIndex((diffFile) => diffFile.file_hash === sha1InHash);
+ const file = this.diffs[idx];
+
+ this.loadCollapsedDiff({ file })
+ .then(() => {
+ this.setDiscussions();
+ this.scrollVirtualScrollerToIndex(idx);
+ this.setFileForcedOpen({ filePath: file.new_path });
+ })
+ .catch(() => {});
+ }
+ },
navigateToDiffFileNumber(number) {
this.navigateToDiffFileIndex(number - 1);
},
@@ -445,7 +508,7 @@ export default {
this.fetchCoverageFiles();
}
- if (this.endpointCodequality) {
+ if (this.endpointCodequality && !this.sastReportsInInlineDiff) {
this.fetchCodequality();
}
@@ -623,9 +686,13 @@ export default {
page-mode
>
<template #default="{ item, index, active }">
- <dynamic-scroller-item :item="item" :active="active" :class="{ active }">
+ <dynamic-scroller-item
+ v-if="active"
+ :item="item"
+ :active="active"
+ :class="{ active }"
+ >
<diff-file
- v-if="active"
:file="item"
:reviewed="fileReviews[item.id]"
:is-first-file="index === 0"
diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
index 4501988ee4f..74b872d8bc4 100644
--- a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
+++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
@@ -26,7 +26,7 @@ export default {
<template>
<gl-dropdown
:text="selectedVersionName"
- data-qa-selector="dropdown_content"
+ data-testid="version-dropdown-content"
size="small"
category="tertiary"
>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index bc2376fec09..13388307955 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -90,7 +90,7 @@ export default {
variant="default"
icon="file-tree"
class="gl-mr-3 js-toggle-tree-list btn-icon"
- data-qa-selector="file_tree_button"
+ data-testid="file-tree-button"
:title="toggleFileBrowserTitle"
:aria-label="toggleFileBrowserTitle"
:selected="showTreeList"
@@ -141,7 +141,7 @@ export default {
<compare-dropdown-layout
:versions="diffCompareDropdownTargetVersions"
class="mr-version-compare-dropdown"
- data-qa-selector="target_version_dropdown"
+ data-testid="target-version-dropdown"
/>
</template>
<template #source>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index f99edced361..c74a4b47fcb 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -161,6 +161,9 @@ export default {
manuallyCollapsed() {
return collapsedType(this.file) === DIFF_FILE_MANUAL_COLLAPSE;
},
+ forcedOpen() {
+ return this.file.viewer.forceOpen;
+ },
showBody() {
return !this.isCollapsed || this.automaticallyCollapsed;
},
@@ -174,6 +177,10 @@ export default {
return Boolean(gon.current_user_id);
},
isCollapsed() {
+ if (this.forcedOpen) {
+ return false;
+ }
+
if (collapsedType(this.file) !== DIFF_FILE_MANUAL_COLLAPSE) {
return this.viewDiffsFileByFile ? false : this.file.viewer?.automaticallyCollapsed;
}
@@ -201,6 +208,11 @@ export default {
this.manageViewedEffects();
},
},
+ 'file.viewer.forceOpen': {
+ handler: function fileForcedOpenHandler() {
+ this.handleToggle();
+ },
+ },
'file.file_hash': {
handler: function hashChangeWatch(newHash, oldHash) {
if (
@@ -390,23 +402,23 @@ export default {
<div
v-if="idState.forkMessageVisible"
- class="js-file-fork-suggestion-section file-fork-suggestion"
+ class="js-file-fork-suggestion-section file-fork-suggestion gl-border-1 gl-border-solid gl-border-gray-100 gl-border-top-0"
>
<span v-safe-html="forkMessage" class="file-fork-suggestion-note"></span>
<gl-button
:href="file.fork_path"
- class="js-fork-suggestion-button"
+ class="js-fork-suggestion-button gl-mr-3"
category="secondary"
variant="confirm"
>{{ $options.i18n.fork }}</gl-button
>
- <button
- class="js-cancel-fork-suggestion-button btn btn-grouped"
- type="button"
+ <gl-button
+ class="js-cancel-fork-suggestion-button"
+ category="secondary"
@click="hideForkMessage"
>
{{ $options.i18n.cancel }}
- </button>
+ </gl-button>
</div>
<template v-else>
<div
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index a9e63ad53bb..20f82500a02 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -232,10 +232,15 @@ export default {
'setCurrentFileHash',
'reviewFile',
'setFileCollapsedByUser',
+ 'setFileForcedOpen',
'setGenerateTestFilePath',
'toggleFileCommentForm',
]),
handleToggleFile() {
+ this.setFileForcedOpen({
+ filePath: this.diffFile.file_path,
+ forced: false,
+ });
this.$emit('toggleFile');
},
showForkMessage(e) {
@@ -278,6 +283,10 @@ export default {
}
if ((open && reviewed) || (closed && !reviewed)) {
+ this.setFileForcedOpen({
+ filePath: this.diffFile.file_path,
+ forced: false,
+ });
this.$emit('toggleFile');
}
},
@@ -293,7 +302,7 @@ export default {
'is-sidebar-moved': glFeatures.movedMrSidebar,
}"
class="js-file-title file-title file-title-flex-parent gl-border"
- data-qa-selector="file_title_container"
+ data-testid="file-title-container"
:data-qa-file-name="filePath"
@click.self="handleToggleFile"
>
@@ -423,7 +432,7 @@ export default {
right
toggle-class="btn-icon js-diff-more-actions"
class="gl-pt-0!"
- data-qa-selector="dropdown_button"
+ data-testid="options-dropdown-button"
lazy
@show="setMoreActionsShown(true)"
@hidden="setMoreActionsShown(false)"
@@ -450,7 +459,7 @@ export default {
ref="ideEditButton"
:href="diffFile.ide_edit_path"
class="js-ide-edit-blob"
- data-qa-selector="edit_in_ide_button"
+ data-testid="edit-in-ide-button"
target="_blank"
>
{{ __('Open in Web IDE') }}
@@ -482,7 +491,7 @@ export default {
<gl-dropdown-item
v-if="diffHasDiscussions(diffFile)"
ref="toggleDiscussionsButton"
- data-qa-selector="toggle_comments_button"
+ data-testid="toggle-comments-button"
@click="toggleFileDiscussionWrappers(diffFile)"
>
<template v-if="diffHasExpandedDiscussions(diffFile)">
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index ee6e9a2fc94..3dad7a1a8e4 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -248,7 +248,6 @@ export default {
:class="$options.classNameMapCellLeft(props)"
data-testid="left-line-number"
class="diff-td diff-line-num"
- data-qa-selector="new_diff_line_link"
>
<span
v-if="
@@ -266,7 +265,6 @@ export default {
:draggable="!props.line.left.commentsDisabled"
type="button"
class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button"
- data-qa-selector="diff_comment_button"
:disabled="props.line.left.commentsDisabled"
:aria-disabled="props.line.left.commentsDisabled"
@click="
diff --git a/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql
new file mode 100644
index 00000000000..b6920d0f6ec
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql
@@ -0,0 +1,46 @@
+query getMRCodequalityReports($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ title
+ codequalityReportsComparer {
+ report {
+ status
+ newErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ resolvedErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ existingErrors {
+ description
+ fingerprint
+ severity
+ filePath
+ line
+ webUrl
+ engineName
+ }
+ summary {
+ errored
+ resolved
+ total
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 7a661d51c9b..f4715c591b2 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -164,11 +164,7 @@ export default {
</script>
<template>
- <div
- ref="wrapper"
- class="tree-list-holder d-flex flex-column"
- data-qa-selector="file_tree_container"
- >
+ <div ref="wrapper" class="tree-list-holder d-flex flex-column" data-testid="file-tree-container">
<div class="gl-pb-3 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<gl-icon name="search" class="gl-absolute gl-top-3 gl-left-3 tree-list-icon" />
@@ -181,7 +177,6 @@ export default {
name="diff-tree-search"
class="form-control"
data-testid="diff-tree-search"
- data-qa-selector="diff_tree_search"
/>
<button
v-show="search"
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 49f25416585..c0b6c8159dc 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState, mapGetters } from 'vuex';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import { getCookie, parseBoolean, removeCookie } from '~/lib/utils/common_utils';
import notesStore from '~/mr_notes/stores';
@@ -31,6 +32,8 @@ export default function initDiffsApp(store = notesStore) {
},
data() {
return {
+ projectPath: dataset.projectPath || '',
+ iid: dataset.iid || '',
endpointCoverage: dataset.endpointCoverage || '',
endpointCodequality: dataset.endpointCodequality || '',
endpointSast: dataset.endpointSast || '',
@@ -79,6 +82,8 @@ export default function initDiffsApp(store = notesStore) {
render(createElement) {
return createElement('diffs-app', {
props: {
+ projectPath: cleanLeadingSeparator(this.projectPath),
+ iid: this.iid,
endpointCoverage: this.endpointCoverage,
endpointCodequality: this.endpointCodequality,
endpointSast: this.endpointSast,
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 7c68b5f69f1..ed8ae795bda 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -254,6 +254,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
if (totalLoaded === pagination.total_pages || pagination.total_pages === null) {
commit(types.SET_RETRIEVING_BATCHES, false);
+ eventHub.$emit('doneLoadingBatches');
// We need to check that the currentDiffFileId points to a file that exists
if (
@@ -879,6 +880,7 @@ export function switchToFullDiffFromRenamedFile({ commit }, { diffFile }) {
...diffFile.alternate_viewer,
automaticallyCollapsed: false,
manuallyCollapsed: false,
+ forceOpen: false,
},
});
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines });
@@ -893,6 +895,10 @@ export const setFileCollapsedAutomatically = ({ commit }, { filePath, collapsed
commit(types.SET_FILE_COLLAPSED, { filePath, collapsed, trigger: DIFF_FILE_AUTOMATIC_COLLAPSE });
};
+export function setFileForcedOpen({ commit }, { filePath, forced }) {
+ commit(types.SET_FILE_FORCED_OPEN, { filePath, forced });
+}
+
export const setSuggestPopoverDismissed = ({ commit, state }) =>
axios
.post(state.dismissEndpoint, {
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 3df491503a4..c2177bacbcc 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -39,6 +39,7 @@ export const REQUEST_FULL_DIFF = 'REQUEST_FULL_DIFF';
export const RECEIVE_FULL_DIFF_SUCCESS = 'RECEIVE_FULL_DIFF_SUCCESS';
export const RECEIVE_FULL_DIFF_ERROR = 'RECEIVE_FULL_DIFF_ERROR';
export const SET_FILE_COLLAPSED = 'SET_FILE_COLLAPSED';
+export const SET_FILE_FORCED_OPEN = 'SET_FILE_FORCED_OPEN';
export const SET_CURRENT_VIEW_DIFF_FILE_LINES = 'SET_CURRENT_VIEW_DIFF_FILE_LINES';
export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINES';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 3af2d6ee6b1..31369b169f5 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -349,6 +349,11 @@ export default {
}
}
},
+ [types.SET_FILE_FORCED_OPEN](state, { filePath, forced = true }) {
+ const file = state.diffFiles.find((f) => f.file_path === filePath);
+
+ Vue.set(file.viewer, 'forceOpen', forced);
+ },
[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
const file = state.diffFiles.find((f) => f.file_path === filePath);
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 307c41a98f8..15d2ab71bc8 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -18,8 +18,7 @@ import {
EXPANDED_LINE_TYPE,
} from '../constants';
import { prepareRawDiffFile } from '../utils/diff_file';
-
-const SHA1 = /\b([a-f0-9]{40})\b/;
+import { extractFileHash } from '../utils/merge_request';
export const isAdded = (line) => ['new', 'new-nonewline'].includes(line.type);
export const isRemoved = (line) => ['old', 'old-nonewline'].includes(line.type);
@@ -571,14 +570,16 @@ export function isUrlHashFileHeader(urlHash = '') {
}
export function parseUrlHashAsFileHash(urlHash = '', currentDiffFileId = '') {
- const isNoteLink = isUrlHashNoteLink(urlHash);
- let id = urlHash.replace(/^#/, '');
+ const hashless = urlHash.replace(/^#/, '');
+ const isNoteLink = isUrlHashNoteLink(hashless);
+ const extractedSha1 = extractFileHash({ input: hashless });
+ let id = extractedSha1;
if (isNoteLink && currentDiffFileId) {
id = currentDiffFileId;
- } else if (isUrlHashFileHeader(urlHash)) {
- id = id.replace('diff-content-', '');
- } else if (!SHA1.test(id) || isNoteLink) {
+ } else if (isUrlHashFileHeader(hashless)) {
+ id = hashless.replace('diff-content-', '');
+ } else if (!extractedSha1 || isNoteLink) {
id = null;
}
diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js
index 98e1c1cc849..f20ae6464ae 100644
--- a/app/assets/javascripts/diffs/utils/diff_file.js
+++ b/app/assets/javascripts/diffs/utils/diff_file.js
@@ -35,6 +35,7 @@ function collapsed(file) {
return {
automaticallyCollapsed: viewer.automaticallyCollapsed || viewer.collapsed || false,
manuallyCollapsed: null,
+ forceOpen: false,
};
}
diff --git a/app/assets/javascripts/diffs/utils/merge_request.js b/app/assets/javascripts/diffs/utils/merge_request.js
index bc81c0b0a05..a74c9fe7fac 100644
--- a/app/assets/javascripts/diffs/utils/merge_request.js
+++ b/app/assets/javascripts/diffs/utils/merge_request.js
@@ -1,6 +1,7 @@
import { ZERO_CHANGES_ALT_DISPLAY } from '../constants';
const endpointRE = /^(\/?(.+\/)+(.+)\/-\/merge_requests\/(\d+)).*$/i;
+const SHA1RE = /([a-f0-9]{40})/g;
function getVersionInfo({ endpoint } = {}) {
const dummyRoot = 'https://gitlab.com';
@@ -51,3 +52,9 @@ export function getDerivedMergeRequestInformation({ endpoint } = {}) {
startSha,
};
}
+
+export function extractFileHash({ input = '' } = {}) {
+ const matches = input.match(SHA1RE);
+
+ return matches?.[0];
+}
diff --git a/app/assets/javascripts/diffs/utils/sort_findings_by_file.js b/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
new file mode 100644
index 00000000000..3a285e80ace
--- /dev/null
+++ b/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
@@ -0,0 +1,17 @@
+export function sortFindingsByFile(newErrors = []) {
+ const files = {};
+ newErrors.forEach(({ filePath, line, description, severity }) => {
+ if (!files[filePath]) {
+ files[filePath] = [];
+ }
+ files[filePath].push({ line, description, severity: severity.toLowerCase() });
+ });
+
+ const sortedFiles = Object.keys(files)
+ .sort()
+ .reduce((acc, key) => {
+ acc[key] = files[key];
+ return acc;
+ }, {});
+ return { files: sortedFiles };
+}
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 2dba919cf58..0420ffb82f5 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -9,22 +9,27 @@
"format": "uri"
},
"image": {
- "$ref": "#/definitions/image"
+ "$ref": "#/definitions/image",
+ "markdownDescription": "Defining `image` globally is deprecated. Use [`default`](https://docs.gitlab.com/ee/ci/yaml/#default) instead. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#globally-defined-image-services-cache-before_script-after_script)."
},
"services": {
- "$ref": "#/definitions/services"
+ "$ref": "#/definitions/services",
+ "markdownDescription": "Defining `services` globally is deprecated. Use [`default`](https://docs.gitlab.com/ee/ci/yaml/#default) instead. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#globally-defined-image-services-cache-before_script-after_script)."
},
"before_script": {
- "$ref": "#/definitions/before_script"
+ "$ref": "#/definitions/before_script",
+ "markdownDescription": "Defining `before_script` globally is deprecated. Use [`default`](https://docs.gitlab.com/ee/ci/yaml/#default) instead. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#globally-defined-image-services-cache-before_script-after_script)."
},
"after_script": {
- "$ref": "#/definitions/after_script"
+ "$ref": "#/definitions/after_script",
+ "markdownDescription": "Defining `after_script` globally is deprecated. Use [`default`](https://docs.gitlab.com/ee/ci/yaml/#default) instead. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#globally-defined-image-services-cache-before_script-after_script)."
},
"variables": {
"$ref": "#/definitions/globalVariables"
},
"cache": {
- "$ref": "#/definitions/cache"
+ "$ref": "#/definitions/cache",
+ "markdownDescription": "Defining `cache` globally is deprecated. Use [`default`](https://docs.gitlab.com/ee/ci/yaml/#default) instead. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#globally-defined-image-services-cache-before_script-after_script)."
},
"!reference": {
"$ref": "#/definitions/!reference"
@@ -744,39 +749,61 @@
}
}
},
- "before_script": {
- "type": "array",
- "markdownDescription": "Defines scripts that should run *before* the job. Can be set globally or per job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#before_script).",
- "items": {
- "anyOf": [
- {
- "type": "string"
+ "script": {
+ "oneOf": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
},
- {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "minItems": 1
+ }
+ ]
+ },
+ "optional_script": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
}
- ]
- }
+ }
+ ]
+ },
+ "before_script": {
+ "$ref": "#/definitions/optional_script",
+ "markdownDescription": "Defines scripts that should run *before* the job. Can be set globally or per job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#before_script)."
},
"after_script": {
- "type": "array",
- "markdownDescription": "Defines scripts that should run *after* the job. Can be set globally or per job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#after_script).",
- "items": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- ]
- }
+ "$ref": "#/definitions/optional_script",
+ "markdownDescription": "Defines scripts that should run *after* the job. Can be set globally or per job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#after_script)."
},
"rules": {
"type": [
@@ -1508,30 +1535,8 @@
"$ref": "#/definitions/secrets"
},
"script": {
- "markdownDescription": "Shell scripts executed by the Runner. The only required property of jobs. Be careful with special characters (e.g. `:`, `{`, `}`, `&`) and use single or double quotes to avoid issues. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#script)",
- "oneOf": [
- {
- "type": "string",
- "minLength": 1
- },
- {
- "type": "array",
- "items": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- ]
- },
- "minItems": 1
- }
- ]
+ "$ref": "#/definitions/script",
+ "markdownDescription": "Shell scripts executed by the Runner. The only required property of jobs. Be careful with special characters (e.g. `:`, `{`, `}`, `&`) and use single or double quotes to avoid issues. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#script)"
},
"stage": {
"description": "Define what stage the job will run in.",
@@ -2145,30 +2150,8 @@
"markdownDescription": "Specifies lists of commands to execute on the runner at certain stages of job execution. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#hooks).",
"properties": {
"pre_get_sources_script": {
- "markdownDescription": "Specifies a list of commands to execute on the runner before updating the Git repository and any submodules. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#hookspre_get_sources_script).",
- "oneOf": [
- {
- "type": "string",
- "minLength": 1
- },
- {
- "type": "array",
- "items": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- ]
- },
- "minItems": 1
- }
- ]
+ "$ref": "#/definitions/optional_script",
+ "markdownDescription": "Specifies a list of commands to execute on the runner before updating the Git repository and any submodules. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#hookspre_get_sources_script)."
}
},
"additionalProperties": false
diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js
index d585dc009e6..0c9315701eb 100644
--- a/app/assets/javascripts/editor/source_editor.js
+++ b/app/assets/javascripts/editor/source_editor.js
@@ -1,5 +1,4 @@
import { editor as monacoEditor, Uri } from 'monaco-editor';
-import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import languages from '~/ide/lib/languages';
import { registerLanguages } from '~/ide/utils';
@@ -128,9 +127,7 @@ export default class SourceEditor {
this.extensionsStore,
);
- waitForCSSLoaded(() => {
- instance.layout();
- });
+ instance.layout();
let model;
const language = instanceOptions.language || getBlobLanguage(blobPath);
diff --git a/app/assets/javascripts/emoji/awards_app/index.js b/app/assets/javascripts/emoji/awards_app/index.js
index f3d72c2dba5..965cb4f421a 100644
--- a/app/assets/javascripts/emoji/awards_app/index.js
+++ b/app/assets/javascripts/emoji/awards_app/index.js
@@ -9,14 +9,18 @@ export default (el) => {
if (!el) return null;
const {
- dataset: { path },
+ dataset: { path, newCustomEmojiPath },
} = el;
const canAwardEmoji = parseBoolean(el.dataset.canAwardEmoji);
+ const showDefaultAwardEmojis = parseBoolean(el.dataset.showDefaultAwardEmojis);
return new Vue({
el,
name: 'AwardsListRoot',
store: createstore(),
+ provide: {
+ newCustomEmojiPath,
+ },
computed: {
...mapState(['currentUserId', 'canAwardEmoji', 'awards']),
},
@@ -35,7 +39,7 @@ export default (el) => {
awards: this.awards,
canAwardEmoji: this.canAwardEmoji,
currentUserId: this.currentUserId,
- defaultAwards: ['thumbsup', 'thumbsdown'],
+ defaultAwards: showDefaultAwardEmojis ? ['thumbsup', 'thumbsdown'] : [],
selectedClass: 'selected',
},
on: {
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index fcc54f17466..238f0d81b22 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -1,6 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
-import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
+import { GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { findLastIndex } from 'lodash';
import VirtualList from 'vue-virtual-scroll-list';
import { CATEGORY_NAMES, getEmojiCategoryMap, state } from '~/emoji';
@@ -13,11 +13,17 @@ export default {
components: {
GlIcon,
GlDropdown,
+ GlDropdownItem,
GlSearchBoxByType,
VirtualList,
Category,
EmojiList,
},
+ inject: {
+ newCustomEmojiPath: {
+ default: '',
+ },
+ },
props: {
toggleClass: {
type: [Array, String, Object],
@@ -167,6 +173,11 @@ export default {
</virtual-list>
</template>
</emoji-list>
+ <template v-if="newCustomEmojiPath" #footer>
+ <gl-dropdown-item :href="newCustomEmojiPath">
+ {{ __('Create new emoji') }}
+ </gl-dropdown-item>
+ </template>
</gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/environments/components/canary_ingress.vue b/app/assets/javascripts/environments/components/canary_ingress.vue
index 30f3f9dfc75..ef3c6210ce1 100644
--- a/app/assets/javascripts/environments/components/canary_ingress.vue
+++ b/app/assets/javascripts/environments/components/canary_ingress.vue
@@ -1,16 +1,12 @@
<script>
-import { GlDropdown, GlDropdownItem, GlModalDirective as GlModal } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import uniqueId from 'lodash/uniqueId';
import { s__ } from '~/locale';
import { CANARY_UPDATE_MODAL } from '../constants';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
- },
- directives: {
- GlModal,
+ GlCollapsibleListbox,
},
props: {
canaryIngress: {
@@ -25,8 +21,10 @@ export default {
},
ingressOptions: Array(100 / 5 + 1)
.fill(0)
- .map((_, i) => i * 5),
-
+ .map((_, i) => {
+ const value = i * 5;
+ return { value, text: value.toString() };
+ }),
translations: {
stableLabel: s__('CanaryIngress|Stable'),
canaryLabel: s__('CanaryIngress|Canary'),
@@ -59,17 +57,19 @@ export default {
return this.canaryIngress.canary_weight;
},
stableWeight() {
- return (100 - this.weight).toString();
+ return 100 - this.weight;
},
canaryWeight() {
- return this.weight.toString();
+ return this.weight;
},
},
methods: {
changeCanary(weight) {
+ this.$root.$emit('bv::show::modal', CANARY_UPDATE_MODAL);
this.$emit('change', weight);
},
changeStable(weight) {
+ this.$root.$emit('bv::show::modal', CANARY_UPDATE_MODAL);
this.$emit('change', 100 - weight);
},
},
@@ -81,40 +81,27 @@ export default {
<label :for="stableWeightId" :class="$options.css.label" class="gl-rounded-top-left-base">
{{ $options.translations.stableLabel }}
</label>
- <gl-dropdown
+ <gl-collapsible-listbox
:id="stableWeightId"
- :text="stableWeight"
- data-testid="stable-weight"
- class="gl-w-full"
- toggle-class="gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- >
- <gl-dropdown-item
- v-for="option in $options.ingressOptions"
- :key="option"
- v-gl-modal="$options.CANARY_UPDATE_MODAL"
- @click="changeStable(option)"
- >{{ option }}</gl-dropdown-item
- >
- </gl-dropdown>
+ :selected="stableWeight"
+ :items="$options.ingressOptions"
+ class="gl-min-w-full gl-text-black-normal"
+ toggle-class="gl-min-w-full gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
+ @select="changeStable"
+ />
</div>
<div class="gl-display-flex gl-display-flex gl-flex-direction-column">
<label :for="canaryWeightId" :class="$options.css.label" class="gl-rounded-top-right-base">{{
$options.translations.canaryLabel
}}</label>
- <gl-dropdown
+ <gl-collapsible-listbox
:id="canaryWeightId"
- :text="canaryWeight"
- data-testid="canary-weight"
- toggle-class="gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-left-none! gl-border-l-none!"
- >
- <gl-dropdown-item
- v-for="option in $options.ingressOptions"
- :key="option"
- v-gl-modal="$options.CANARY_UPDATE_MODAL"
- @click="changeCanary(option)"
- >{{ option }}</gl-dropdown-item
- >
- </gl-dropdown>
+ :selected="canaryWeight"
+ :items="$options.ingressOptions"
+ class="gl-min-w-full"
+ toggle-class="gl-min-w-full gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
+ @select="changeCanary"
+ />
</div>
</section>
</template>
diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue
index 3ac32f0d045..5b9cc2f3d21 100644
--- a/app/assets/javascripts/environments/components/empty_state.vue
+++ b/app/assets/javascripts/environments/components/empty_state.vue
@@ -42,7 +42,7 @@ export default {
};
</script>
<template>
- <gl-empty-state class="gl-layout-w-limited" :title="title">
+ <gl-empty-state class="gl-layout-w-limited gl-mx-auto" :title="title">
<template #description>
<gl-sprintf :message="content">
<template #link="{ content: contentToDisplay }">
@@ -51,10 +51,10 @@ export default {
</gl-sprintf>
</template>
<template v-if="!hasTerm" #actions>
- <gl-button :href="newEnvironmentPath" variant="confirm">
+ <gl-button class="gl-mx-2 gl-mb-3" :href="newEnvironmentPath" variant="confirm">
{{ $options.i18n.newEnvironmentButtonLabel }}
</gl-button>
- <gl-button @click="$emit('enable-review')">
+ <gl-button class="gl-mx-2 gl-mb-3" @click="$emit('enable-review')">
{{ $options.i18n.enablingReviewButtonLabel }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index 846f2cf73b2..8ebba0e27bb 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -190,13 +190,12 @@ export default {
}
return {
basePath: this.kasTunnelUrl,
- baseOptions: {
- headers: {
- 'GitLab-Agent-Id': getIdFromGraphQLId(this.selectedAgentId),
- ...csrf.headers,
- },
- withCredentials: true,
+ headers: {
+ 'GitLab-Agent-Id': getIdFromGraphQLId(this.selectedAgentId),
+ 'Content-Type': 'application/json',
+ ...csrf.headers,
},
+ credentials: 'include',
};
},
},
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index 0e52a80c2c5..252ced6391d 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -61,10 +61,12 @@ export default {
k8sAccessConfiguration() {
return {
basePath: this.kasTunnelUrl,
- baseOptions: {
- headers: { 'GitLab-Agent-Id': this.gitlabAgentId, ...csrf.headers },
- withCredentials: true,
+ headers: {
+ 'GitLab-Agent-Id': this.gitlabAgentId,
+ 'Content-Type': 'application/json',
+ ...csrf.headers,
},
+ credentials: 'include',
};
},
clusterHealthStatus() {
diff --git a/app/assets/javascripts/environments/components/kubernetes_pods.vue b/app/assets/javascripts/environments/components/kubernetes_pods.vue
index aded3a4d0c4..3f040f1f40a 100644
--- a/app/assets/javascripts/environments/components/kubernetes_pods.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_pods.vue
@@ -23,7 +23,7 @@ export default {
return data?.k8sPods || [];
},
error(error) {
- this.error = error;
+ this.error = error.message;
this.$emit('cluster-error', this.error);
},
watchLoading(isLoading) {
diff --git a/app/assets/javascripts/environments/components/kubernetes_summary.vue b/app/assets/javascripts/environments/components/kubernetes_summary.vue
index b00e82809f6..e2fbc6fd2e7 100644
--- a/app/assets/javascripts/environments/components/kubernetes_summary.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_summary.vue
@@ -30,7 +30,7 @@ export default {
return data?.k8sWorkloads || {};
},
error(error) {
- this.$emit('cluster-error', error);
+ this.$emit('cluster-error', error.message);
},
result() {
this.checkFailed();
diff --git a/app/assets/javascripts/environments/components/kubernetes_tabs.vue b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
index 4492d209e3b..7c699eec412 100644
--- a/app/assets/javascripts/environments/components/kubernetes_tabs.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
@@ -24,13 +24,14 @@ export default {
variables() {
return {
configuration: this.configuration,
+ namespace: this.namespace,
};
},
update(data) {
return data?.k8sServices || [];
},
error(error) {
- this.$emit('cluster-error', error);
+ this.$emit('cluster-error', error.message);
},
},
},
@@ -139,6 +140,7 @@ export default {
:configuration="configuration"
@loading="$emit('loading', $event)"
@failed="$emit('failed')"
+ @cluster-error="$emit('cluster-error', $event)"
/>
<gl-tab>
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index 149cab21acd..aacb460a817 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -185,7 +185,7 @@ export default {
},
update(data) {
this.clusterAgent = data?.project?.environment?.clusterAgent;
- this.kubernetesNamespace = data?.project?.environment?.kubernetesNamespace;
+ this.kubernetesNamespace = data?.project?.environment?.kubernetesNamespace || '';
this.fluxResourcePath = data?.project?.environment?.fluxResourcePath || '';
},
});
diff --git a/app/assets/javascripts/environments/environment_details/deployments_table.vue b/app/assets/javascripts/environments/environment_details/deployments_table.vue
index 261d8106438..e46411f4d2c 100644
--- a/app/assets/javascripts/environments/environment_details/deployments_table.vue
+++ b/app/assets/javascripts/environments/environment_details/deployments_table.vue
@@ -48,12 +48,17 @@ export default {
<deployment-job :job="item.job" />
</template>
<template #cell(created)="{ item }">
- <time-ago-tooltip :time="item.created" data-testid="deployment-created-at" />
+ <time-ago-tooltip
+ :time="item.created"
+ enable-truncation
+ data-testid="deployment-created-at"
+ />
</template>
<template #cell(deployed)="{ item }">
<time-ago-tooltip
v-if="item.deployed"
:time="item.deployed"
+ enable-truncation
data-testid="deployment-deployed-at"
/>
</template>
diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue
index aa836299bcc..6e3ec04ba3b 100644
--- a/app/assets/javascripts/environments/environment_details/index.vue
+++ b/app/assets/javascripts/environments/environment_details/index.vue
@@ -150,10 +150,10 @@ export default {
},
},
errorCaptured(error) {
- Sentry.withScope((scope) => {
- scope.setTag('vue_component', 'EnvironmentDetailsIndex');
-
- Sentry.captureException(error);
+ Sentry.captureException(error, {
+ tags: {
+ vue_component: 'EnvironmentDetailsIndex',
+ },
});
},
mounted() {
diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql
index d97849eecc1..8fc4a54b08b 100644
--- a/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/k8s_services.query.graphql
@@ -1,5 +1,5 @@
-query getK8sServices($configuration: LocalConfiguration) {
- k8sServices(configuration: $configuration) @client {
+query getK8sServices($configuration: LocalConfiguration, $namespace: String) {
+ k8sServices(configuration: $configuration, namespace: $namespace) @client {
metadata {
name
namespace
diff --git a/app/assets/javascripts/environments/graphql/resolvers/flux.js b/app/assets/javascripts/environments/graphql/resolvers/flux.js
index f9ca35a3165..d39b1bed7b6 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/flux.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/flux.js
@@ -23,7 +23,7 @@ const buildFluxResourceUrl = ({
};
const getFluxResourceStatus = (configuration, url) => {
- const { headers } = configuration.baseOptions;
+ const { headers } = configuration;
const withCredentials = true;
return axios
@@ -37,7 +37,7 @@ const getFluxResourceStatus = (configuration, url) => {
};
const getFluxResources = (configuration, url) => {
- const { headers } = configuration.baseOptions;
+ const { headers } = configuration;
const withCredentials = true;
return axios
diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
index 9ab65d0bb7f..67a472dac93 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
@@ -44,30 +44,41 @@ const mapWorkloadItems = (items, kind) => {
});
};
-const handleClusterError = (err) => {
- const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
- throw error;
+const handleClusterError = async (err) => {
+ if (!err.response) {
+ throw err;
+ }
+
+ const errorData = await err.response.json();
+ throw errorData;
};
export default {
k8sPods(_, { configuration, namespace }) {
const coreV1Api = new CoreV1Api(new Configuration(configuration));
const podsApi = namespace
- ? coreV1Api.listCoreV1NamespacedPod(namespace)
+ ? coreV1Api.listCoreV1NamespacedPod({ namespace })
: coreV1Api.listCoreV1PodForAllNamespaces();
return podsApi
- .then((res) => res?.data?.items || [])
- .catch((err) => {
- handleClusterError(err);
+ .then((res) => res?.items || [])
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
});
},
- k8sServices(_, { configuration }) {
+ k8sServices(_, { configuration, namespace }) {
const coreV1Api = new CoreV1Api(new Configuration(configuration));
- return coreV1Api
- .listCoreV1ServiceForAllNamespaces()
+ const servicesApi = namespace
+ ? coreV1Api.listCoreV1NamespacedService({ namespace })
+ : coreV1Api.listCoreV1ServiceForAllNamespaces();
+
+ return servicesApi
.then((res) => {
- const items = res?.data?.items || [];
+ const items = res?.items || [];
return items.map((item) => {
const { type, clusterIP, externalIP, ports } = item.spec;
return {
@@ -81,24 +92,28 @@ export default {
};
});
})
- .catch((err) => {
- handleClusterError(err);
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
});
},
k8sWorkloads(_, { configuration, namespace }) {
- const appsV1api = new AppsV1Api(configuration);
- const batchV1api = new BatchV1Api(configuration);
+ const appsV1api = new AppsV1Api(new Configuration(configuration));
+ const batchV1api = new BatchV1Api(new Configuration(configuration));
let promises;
if (namespace) {
promises = [
- appsV1api.listAppsV1NamespacedDeployment(namespace),
- appsV1api.listAppsV1NamespacedDaemonSet(namespace),
- appsV1api.listAppsV1NamespacedStatefulSet(namespace),
- appsV1api.listAppsV1NamespacedReplicaSet(namespace),
- batchV1api.listBatchV1NamespacedJob(namespace),
- batchV1api.listBatchV1NamespacedCronJob(namespace),
+ appsV1api.listAppsV1NamespacedDeployment({ namespace }),
+ appsV1api.listAppsV1NamespacedDaemonSet({ namespace }),
+ appsV1api.listAppsV1NamespacedStatefulSet({ namespace }),
+ appsV1api.listAppsV1NamespacedReplicaSet({ namespace }),
+ batchV1api.listBatchV1NamespacedJob({ namespace }),
+ batchV1api.listBatchV1NamespacedCronJob({ namespace }),
];
} else {
promises = [
@@ -120,15 +135,18 @@ export default {
CronJobList: [],
};
- return Promise.allSettled(promises).then((results) => {
+ return Promise.allSettled(promises).then(async (results) => {
if (results.every((res) => res.status === 'rejected')) {
const error = results[0].reason;
- const errorMessage = error?.response?.data?.message ?? error;
- throw new Error(errorMessage);
+ try {
+ await handleClusterError(error);
+ } catch (err) {
+ throw new Error(err.message);
+ }
}
for (const promiseResult of results) {
- if (promiseResult.status === 'fulfilled' && promiseResult?.value?.data) {
- const { kind, items } = promiseResult.value.data;
+ if (promiseResult.status === 'fulfilled' && promiseResult?.value) {
+ const { kind, items } = promiseResult.value;
if (items?.length > 0) {
summaryList[kind] = mapWorkloadItems(items, kind);
@@ -145,11 +163,14 @@ export default {
return namespacesApi
.then((res) => {
- return res?.data?.items || [];
+ return res?.items || [];
})
- .catch((err) => {
- const error = err?.response?.data?.reason || err;
- throw new Error(humanizeClusterErrors(error));
+ .catch(async (error) => {
+ try {
+ await handleClusterError(error);
+ } catch (err) {
+ throw new Error(humanizeClusterErrors(err.reason));
+ }
});
},
};
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 9b30ec4afbb..4d4bae12570 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -493,7 +493,11 @@ export default {
</div>
<!-- Get Started with ET -->
<div v-else>
- <gl-empty-state :title="__('Get started with error tracking')" :svg-path="illustrationPath">
+ <gl-empty-state
+ :title="__('Get started with error tracking')"
+ :svg-path="illustrationPath"
+ :svg-height="null"
+ >
<template #description>
<div>
<span>{{ __('Monitor your errors directly in GitLab.') }}</span>
diff --git a/app/assets/javascripts/feature_flags/components/empty_state.vue b/app/assets/javascripts/feature_flags/components/empty_state.vue
index 60aeb297700..ccc984ee7a0 100644
--- a/app/assets/javascripts/feature_flags/components/empty_state.vue
+++ b/app/assets/javascripts/feature_flags/components/empty_state.vue
@@ -74,6 +74,7 @@ export default {
:title="errorTitle"
:description="s__('FeatureFlags|Try again in a few moments or contact your support team.')"
:svg-path="errorStateSvgPath"
+ :svg-height="null"
data-testid="error-state"
/>
diff --git a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
index 0fde87dd0ba..7cc87544be9 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
@@ -93,7 +93,7 @@ export default {
type="number"
min="0"
max="100"
- size="xs"
+ width="xs"
@input="onPercentageChange"
/>
<span class="ml-1">%</span>
diff --git a/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
index 0acb0d4366c..a46eee7b130 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
@@ -59,7 +59,7 @@ export default {
type="number"
min="0"
max="100"
- size="xs"
+ width="xs"
@input="onPercentageChange"
/>
<span class="gl-ml-2">%</span>
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 264427f5806..99d22b1330b 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -129,7 +129,7 @@ class GfmAutoComplete {
this.dataSources = dataSources;
this.cachedData = {};
this.isLoadingData = {};
- this.previousQuery = '';
+ this.previousQuery = undefined;
}
setup(input, enableMap = defaultAutocompleteConfig) {
@@ -776,15 +776,19 @@ class GfmAutoComplete {
return $.fn.atwho.default.callbacks.sorter(query, items, searchKey);
},
filter(query, data, searchKey) {
+ if (GfmAutoComplete.isTypeWithBackendFiltering(this.at)) {
+ if (GfmAutoComplete.isLoading(data) || self.previousQuery !== query) {
+ self.previousQuery = query;
+ self.fetchData(this.$inputor, this.at, query);
+ return data;
+ }
+ }
+
if (GfmAutoComplete.isLoading(data)) {
self.fetchData(this.$inputor, this.at);
return data;
}
- if (GfmAutoComplete.isTypeWithBackendFiltering(this.at) && self.previousQuery !== query) {
- self.fetchData(this.$inputor, this.at, query);
- self.previousQuery = query;
- return data;
- }
+
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
},
beforeInsert(value) {
@@ -828,14 +832,18 @@ class GfmAutoComplete {
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
if (GfmAutoComplete.isTypeWithBackendFiltering(at)) {
- axios
- .get(dataSource, { params: { search } })
- .then(({ data }) => {
- this.loadData($input, at, data);
- })
- .catch(() => {
- this.isLoadingData[at] = false;
- });
+ if (this.cachedData[at]?.[search]) {
+ this.loadData($input, at, this.cachedData[at][search], { search });
+ } else {
+ axios
+ .get(dataSource, { params: { search } })
+ .then(({ data }) => {
+ this.loadData($input, at, data, { search });
+ })
+ .catch(() => {
+ this.isLoadingData[at] = false;
+ });
+ }
} else if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
@@ -853,9 +861,19 @@ class GfmAutoComplete {
}
}
- loadData($input, at, data) {
+ loadData($input, at, data, { search } = {}) {
this.isLoadingData[at] = false;
- this.cachedData[at] = data;
+
+ if (search !== undefined) {
+ if (this.cachedData[at] === undefined) {
+ this.cachedData[at] = {};
+ }
+
+ this.cachedData[at][search] = data;
+ } else {
+ this.cachedData[at] = data;
+ }
+
$input.atwho('load', at, data);
// This trigger at.js again
// otherwise we would be stuck with loading until the user types
diff --git a/app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue b/app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue
index 823895214df..f97c1e54094 100644
--- a/app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue
+++ b/app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue
@@ -57,6 +57,7 @@ export default {
:title="$options.i18n.noInstancesTitle"
:description="$options.i18n.noInstancesDescription"
:svg-path="emptyIllustrationUrl"
+ :svg-height="null"
/>
<div v-else>
diff --git a/app/assets/javascripts/google_tag_manager/index.js b/app/assets/javascripts/google_tag_manager/index.js
index a9ae9a5af82..0cb25fbaeb5 100644
--- a/app/assets/javascripts/google_tag_manager/index.js
+++ b/app/assets/javascripts/google_tag_manager/index.js
@@ -1,310 +1 @@
-import { v4 as uuidv4 } from 'uuid';
-import { logError } from '~/lib/logger';
-
-const SKU_PREMIUM = '2c92a00d76f0d5060176f2fb0a5029ff';
-const SKU_ULTIMATE = '2c92a0ff76f0d5250176f2f8c86f305a';
-const PRODUCT_INFO = {
- [SKU_PREMIUM]: {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- name: 'Premium',
- id: '0002',
- price: '228',
- variant: 'SaaS',
- },
- [SKU_ULTIMATE]: {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- name: 'Ultimate',
- id: '0001',
- price: '1188',
- variant: 'SaaS',
- },
-};
-const EMPTY_NAMESPACE_ID_VALUE = 'not available';
-
-const generateProductInfo = (sku, quantity) => {
- const product = PRODUCT_INFO[sku];
-
- if (!product) {
- logError('Unexpected product sku provided to generateProductInfo');
- return {};
- }
-
- const productInfo = {
- ...product,
- brand: 'GitLab',
- category: 'DevOps',
- quantity,
- };
-
- return productInfo;
-};
-
-const isSupported = () => Boolean(window.dataLayer) && gon.features?.gitlabGtmDatalayer;
-// gon.features.gitlabGtmDatalayer is set by writing
-// `push_frontend_feature_flag(:gitlab_gtm_datalayer, type: :ops)`
-// to the appropriate controller
-// window.dataLayer is set by adding partials to the appropriate view found in
-// views/layouts/_google_tag_manager_body.html.haml and _google_tag_manager_head.html.haml
-
-const pushEvent = (event, args = {}) => {
- if (!window.dataLayer) {
- return;
- }
-
- try {
- window.dataLayer.push({
- event,
- ...args,
- });
- } catch (e) {
- logError('Unexpected error while pushing to dataLayer', e);
- }
-};
-
-const pushEnhancedEcommerceEvent = (event, args = {}) => {
- if (!window.dataLayer) {
- return;
- }
-
- try {
- window.dataLayer.push({ ecommerce: null }); // Clear the previous ecommerce object
- window.dataLayer.push({
- event,
- ...args,
- });
- } catch (e) {
- logError('Unexpected error while pushing to dataLayer', e);
- }
-};
-
-const pushAccountSubmit = (accountType, accountMethod) =>
- pushEvent('accountSubmit', { accountType, accountMethod });
-
-const trackFormSubmission = (accountType) => {
- const form = document.getElementById('new_new_user');
- form.addEventListener('submit', () => {
- pushAccountSubmit(accountType, 'form');
- });
-};
-
-const trackOmniAuthSubmission = (accountType) => {
- const links = document.querySelectorAll('.js-oauth-login');
- links.forEach((link) => {
- const { provider } = link.dataset;
- link.addEventListener('click', () => {
- pushAccountSubmit(accountType, provider);
- });
- });
-};
-
-export const trackFreeTrialAccountSubmissions = () => {
- if (!isSupported()) {
- return;
- }
-
- trackFormSubmission('freeThirtyDayTrial');
- trackOmniAuthSubmission('freeThirtyDayTrial');
-};
-
-export const trackNewRegistrations = () => {
- if (!isSupported()) {
- return;
- }
-
- trackFormSubmission('standardSignUp');
- trackOmniAuthSubmission('standardSignUp');
-};
-
-export const trackSaasTrialSubmit = () => {
- if (!isSupported()) {
- return;
- }
-
- pushEvent('saasTrialSubmit');
-};
-
-export const trackSaasTrialGroup = () => {
- if (!isSupported()) {
- return;
- }
-
- const form = document.querySelector('.js-saas-trial-group');
-
- if (!form) return;
-
- form.addEventListener('submit', () => {
- pushEvent('saasTrialGroup');
- });
-};
-
-export const trackProjectImport = () => {
- if (!isSupported()) {
- return;
- }
-
- const importButtons = document.querySelectorAll('.js-import-project-btn');
- importButtons.forEach((button) => {
- button.addEventListener('click', () => {
- const { platform } = button.dataset;
- pushEvent('projectImport', { platform });
- });
- });
-};
-
-export const trackSaasTrialGetStarted = () => {
- if (!isSupported()) {
- return;
- }
-
- const getStartedButton = document.querySelector('.js-get-started-btn');
- getStartedButton.addEventListener('click', () => {
- pushEvent('saasTrialGetStarted');
- });
-};
-
-export const trackTrialAcceptTerms = () => {
- if (!isSupported()) {
- return;
- }
-
- pushEvent('saasTrialAcceptTerms');
-};
-
-export const trackCheckout = (selectedPlan, quantity) => {
- if (!isSupported()) {
- return;
- }
-
- const product = generateProductInfo(selectedPlan, quantity);
-
- if (Object.keys(product).length === 0) {
- return;
- }
-
- const eventData = {
- ecommerce: {
- currencyCode: 'USD',
- checkout: {
- actionField: { step: 1 },
- products: [product],
- },
- },
- };
-
- // eslint-disable-next-line @gitlab/require-i18n-strings
- pushEnhancedEcommerceEvent('EECCheckout', eventData);
-};
-
-export const getNamespaceId = () => {
- return window.gl.snowplowStandardContext?.data?.namespace_id || EMPTY_NAMESPACE_ID_VALUE;
-};
-
-export const trackTransaction = (transactionDetails) => {
- if (!isSupported()) {
- return;
- }
-
- const transactionId = uuidv4();
- const { paymentOption, revenue, tax, selectedPlan, quantity } = transactionDetails;
- const product = generateProductInfo(selectedPlan, quantity);
- const namespaceId = getNamespaceId();
-
- if (Object.keys(product).length === 0) {
- return;
- }
-
- const eventData = {
- ecommerce: {
- currencyCode: 'USD',
- purchase: {
- actionField: {
- id: transactionId,
- affiliation: 'GitLab',
- option: paymentOption,
- revenue: revenue.toString(),
- tax: tax.toString(),
- },
- products: [{ ...product, dimension36: namespaceId }],
- },
- },
- };
-
- pushEnhancedEcommerceEvent('EECtransactionSuccess', eventData);
-};
-
-export const pushEECproductAddToCartEvent = () => {
- if (!isSupported()) {
- return;
- }
-
- window.dataLayer.push({
- event: 'EECproductAddToCart',
- ecommerce: {
- currencyCode: 'USD',
- add: {
- products: [
- {
- name: 'CI/CD Minutes',
- id: '0003',
- price: '10',
- brand: 'GitLab',
- category: 'DevOps',
- variant: 'add-on',
- quantity: 1,
- },
- ],
- },
- },
- });
-};
-
-export const trackAddToCartUsageTab = () => {
- const getStartedButton = document.querySelector('.js-buy-additional-minutes');
- if (!getStartedButton) {
- return;
- }
- getStartedButton.addEventListener('click', pushEECproductAddToCartEvent);
-};
-
-export const trackCombinedGroupProjectForm = () => {
- if (!isSupported()) {
- return;
- }
-
- const form = document.querySelector('.js-groups-projects-form');
- form.addEventListener('submit', () => {
- pushEvent('combinedGroupProjectFormSubmit');
- });
-};
-
-export const trackCompanyForm = (aboutYourCompanyType) => {
- if (!isSupported()) {
- return;
- }
-
- pushEvent('aboutYourCompanyFormSubmit', { aboutYourCompanyType });
-};
-
-export const saasTrialWelcome = () => {
- if (!isSupported()) {
- return;
- }
-
- const saasTrialWelcomeButton = document.querySelector('.js-trial-welcome-btn');
-
- saasTrialWelcomeButton?.addEventListener('click', () => {
- pushEvent('saasTrialWelcome');
- });
-};
-
-export const saasTrialContinuousOnboarding = () => {
- if (!isSupported()) {
- return;
- }
-
- const getStartedButton = document.querySelector('.js-get-started-btn');
-
- getStartedButton?.addEventListener('click', () => {
- pushEvent('saasTrialContinuousOnboarding');
- });
-};
+export const trackTrialAcceptTerms = () => {};
diff --git a/app/assets/javascripts/graphql_shared/client/is_showing_labels.query.graphql b/app/assets/javascripts/graphql_shared/client/is_showing_labels.query.graphql
new file mode 100644
index 00000000000..dc16f7ad313
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/client/is_showing_labels.query.graphql
@@ -0,0 +1,3 @@
+query isShowingLabels {
+ isShowingLabels @client
+}
diff --git a/app/assets/javascripts/graphql_shared/client/set_is_showing_labels.mutation.graphql b/app/assets/javascripts/graphql_shared/client/set_is_showing_labels.mutation.graphql
new file mode 100644
index 00000000000..2f115291977
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/client/set_is_showing_labels.mutation.graphql
@@ -0,0 +1,3 @@
+mutation setIsShowingLabels($isShowingLabels: Boolean!) {
+ setIsShowingLabels(isShowingLabels: $isShowingLabels) @client
+}
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index eb807bc7540..9537c9ef8a6 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -3,6 +3,8 @@ import VueApollo from 'vue-apollo';
import { defaultDataIdFromObject } from '@apollo/client/core';
import { concatPagination } from '@apollo/client/utilities';
import errorQuery from '~/boards/graphql/client/error.query.graphql';
+import selectedBoardItemsQuery from '~/boards/graphql/client/selected_board_items.query.graphql';
+import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import createDefaultClient from '~/lib/graphql';
import typeDefs from '~/work_items/graphql/typedefs.graphql';
@@ -20,6 +22,20 @@ export const config = {
: defaultDataIdFromObject(object);
},
typePolicies: {
+ Query: {
+ fields: {
+ isShowingLabels: {
+ read(currentState) {
+ return currentState ?? true;
+ },
+ },
+ selectedBoardItems: {
+ read(currentState) {
+ return currentState ?? [];
+ },
+ },
+ },
+ },
Project: {
fields: {
projectMembers: {
@@ -77,14 +93,6 @@ export const config = {
const incomingWidget = incoming.find(
(w) => w.type && w.type === existingWidget.type,
);
- // We don't want to override existing notes or award emojis with empty widget on work item updates
- if (
- (incomingWidget?.type === WIDGET_TYPE_NOTES ||
- incomingWidget?.type === WIDGET_TYPE_AWARD_EMOJI) &&
- !context.variables.pageSize
- ) {
- return existingWidget;
- }
// we want to concat next page of awardEmoji to the existing ones
if (incomingWidget?.type === WIDGET_TYPE_AWARD_EMOJI && context.variables.after) {
@@ -116,7 +124,7 @@ export const config = {
};
}
- return incomingWidget || existingWidget;
+ return { ...existingWidget, ...incomingWidget };
});
},
},
@@ -211,6 +219,16 @@ export const config = {
epicBoardList: {
keyArgs: ['id'],
},
+ isShowingLabels: {
+ read(currentState) {
+ return currentState ?? true;
+ },
+ },
+ selectedBoardItems: {
+ read(currentState) {
+ return currentState ?? [];
+ },
+ },
},
},
}
@@ -235,6 +253,21 @@ export const resolvers = {
});
return boardItem;
},
+ setSelectedBoardItems(_, { itemId }, { cache }) {
+ const sourceData = cache.readQuery({ query: selectedBoardItemsQuery });
+ cache.writeQuery({
+ query: selectedBoardItemsQuery,
+ data: { selectedBoardItems: [...sourceData.selectedBoardItems, itemId] },
+ });
+ return [...sourceData.selectedBoardItems, itemId];
+ },
+ unsetSelectedBoardItems(_, _variables, { cache }) {
+ cache.writeQuery({
+ query: selectedBoardItemsQuery,
+ data: { selectedBoardItems: [] },
+ });
+ return [];
+ },
setError(_, { error }, { cache }) {
cache.writeQuery({
query: errorQuery,
@@ -258,6 +291,13 @@ export const resolvers = {
},
};
},
+ setIsShowingLabels(_, { isShowingLabels }, { cache }) {
+ cache.writeQuery({
+ query: isShowingLabelsQuery,
+ data: { isShowingLabels },
+ });
+ return isShowingLabels;
+ },
},
};
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 37c1674cc5a..4e0b1413f71 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -3,6 +3,9 @@
"AlertManagementHttpIntegration",
"AlertManagementPrometheusIntegration"
],
+ "AmazonS3ConfigurationInterface": [
+ "AmazonS3ConfigurationType"
+ ],
"BaseHeaderInterface": [
"AuditEventStreamingHeader",
"AuditEventsStreamingInstanceHeader"
diff --git a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
index 7c88e494a2e..8f45224338f 100644
--- a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
@@ -1,10 +1,21 @@
#import "../fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query groupUsersSearch($search: String!, $fullPath: ID!) {
+query groupUsersSearch($search: String!, $fullPath: ID!, $after: String, $first: Int) {
workspace: group(fullPath: $fullPath) {
id
- users: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
+ users: groupMembers(
+ search: $search
+ relations: [DIRECT, DESCENDANTS, INHERITED]
+ first: $first
+ after: $after
+ sort: USER_FULL_NAME_ASC
+ ) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ startCursor
+ }
nodes {
id
user {
diff --git a/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue
index 470ff45f47a..e74d827af9b 100644
--- a/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue
+++ b/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue
@@ -20,5 +20,6 @@ export default {
:title="$options.i18n.title"
:description="$options.i18n.description"
:svg-path="groupsEmptyStateIllustration"
+ :svg-height="null"
/>
</template>
diff --git a/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue
index f524b769802..0068772ff23 100644
--- a/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue
+++ b/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue
@@ -13,5 +13,9 @@ export default {
</script>
<template>
- <gl-empty-state :title="$options.i18n.title" :svg-path="groupsEmptyStateIllustration" />
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :svg-path="groupsEmptyStateIllustration"
+ :svg-height="null"
+ />
</template>
diff --git a/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue
index 0bd95d59022..841a80b6ce4 100644
--- a/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue
+++ b/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue
@@ -86,6 +86,7 @@ export default {
v-else
:title="$options.i18n.withoutLinks.title"
:svg-path="emptySubgroupIllustration"
+ :svg-height="null"
:description="$options.i18n.withoutLinks.description"
/>
</template>
diff --git a/app/assets/javascripts/groups/components/group_name_and_path.vue b/app/assets/javascripts/groups/components/group_name_and_path.vue
index fd633df3022..853fdd7c55e 100644
--- a/app/assets/javascripts/groups/components/group_name_and_path.vue
+++ b/app/assets/javascripts/groups/components/group_name_and_path.vue
@@ -294,7 +294,7 @@ export default {
:name="fields.name.name"
:placeholder="$options.i18n.inputs.name.placeholder"
data-testid="group-name-field"
- :size="$options.inputSize"
+ :width="$options.inputSize"
:state="nameFeedbackState"
@invalid="handleInvalidName"
/>
@@ -374,7 +374,7 @@ export default {
:maxlength="fields.path.maxLength"
:pattern="fields.path.pattern"
:state="pathFeedbackState"
- :size="pathInputSize"
+ :width="pathInputSize"
required
data-testid="group-path-field"
:data-bind-in="mattermostEnabled ? $options.mattermostDataBindName : null"
@@ -397,7 +397,7 @@ export default {
:id="fields.groupId.id"
:value="fields.groupId.value"
:name="fields.groupId.name"
- size="sm"
+ width="sm"
readonly
/>
</gl-form-group>
diff --git a/app/assets/javascripts/groups/components/transfer_group_form.vue b/app/assets/javascripts/groups/components/transfer_group_form.vue
index 3da417ebf0a..9f968817a3a 100644
--- a/app/assets/javascripts/groups/components/transfer_group_form.vue
+++ b/app/assets/javascripts/groups/components/transfer_group_form.vue
@@ -73,7 +73,7 @@ export default {
:disabled="disableSubmitButton"
:phrase="confirmationPhrase"
:button-text="confirmButtonText"
- button-qa-selector="transfer_group_button"
+ button-testid="transfer-group-button"
@confirm="$emit('confirm')"
/>
</div>
diff --git a/app/assets/javascripts/helpers/startup_css_helper.js b/app/assets/javascripts/helpers/startup_css_helper.js
deleted file mode 100644
index 6e19979721c..00000000000
--- a/app/assets/javascripts/helpers/startup_css_helper.js
+++ /dev/null
@@ -1,36 +0,0 @@
-const CSS_LOADED_EVENT = 'CSSLoaded';
-const STARTUP_LINK_LOADED_EVENT = 'CSSStartupLinkLoaded';
-
-const getAllStartupLinks = (() => {
- let links = null;
- return () => {
- if (!links) {
- links = Array.from(document.querySelectorAll('link[data-startupcss]'));
- }
- return links;
- };
-})();
-const isStartupLinkLoaded = ({ dataset }) => dataset.startupcss === 'loaded';
-const allLinksLoaded = () => getAllStartupLinks().every(isStartupLinkLoaded);
-
-const handleStartupEvents = () => {
- if (allLinksLoaded()) {
- document.dispatchEvent(new CustomEvent(CSS_LOADED_EVENT));
- document.removeEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
- }
-};
-
-/* For `waitForCSSLoaded` methods, see docs.gitlab.com/ee/development/fe_guide/performance.html#important-considerations */
-export const waitForCSSLoaded = (action = () => {}) => {
- if (allLinksLoaded()) {
- return new Promise((resolve) => {
- action();
- resolve();
- });
- }
-
- return new Promise((resolve) => {
- document.addEventListener(CSS_LOADED_EVENT, resolve, { once: true });
- document.addEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents);
- }).then(action);
-};
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 741845e3325..ba1258f8b50 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -176,7 +176,6 @@ export default {
type="text"
class="form-control"
data-testid="file-name-field"
- data-qa-selector="file_name_field"
:placeholder="placeholder"
/>
</form>
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index 2e113003f8a..868830c953a 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -1,9 +1,11 @@
import { start } from '@gitlab/web-ide';
import { __ } from '~/locale';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
import csrf from '~/lib/utils/csrf';
+import Tracking from '~/tracking';
import { getBaseConfig } from './lib/gitlab_web_ide/get_base_config';
import { setupRootElement } from './lib/gitlab_web_ide/setup_root_element';
import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from './constants';
@@ -39,13 +41,14 @@ export const initGitlabWebIDE = async (el) => {
filePath,
mergeRequest: mrId,
forkInfo: forkInfoJSON,
- editorFontSrcUrl,
- editorFontFormat,
- editorFontFamily,
+ editorFont: editorFontJSON,
codeSuggestionsEnabled,
} = el.dataset;
const rootEl = setupRootElement(el);
+ const editorFont = editorFontJSON
+ ? convertObjectPropsToCamelCase(JSON.parse(editorFontJSON), { deep: true })
+ : null;
const forkInfo = forkInfoJSON ? JSON.parse(forkInfoJSON) : null;
// See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17
@@ -69,13 +72,11 @@ export const initGitlabWebIDE = async (el) => {
userPreferences: el.dataset.userPreferencesPath,
signIn: el.dataset.signInPath,
},
- editorFont: {
- srcUrl: editorFontSrcUrl,
- fontFamily: editorFontFamily,
- format: editorFontFormat,
- },
+ editorFont,
codeSuggestionsEnabled,
handleTracking,
+ // See https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L86
+ telemetryEnabled: Tracking.enabled(),
async handleStartRemote({ remoteHost, remotePath, connectionToken }) {
const confirmed = await confirmAction(
__('Are you sure you want to leave the Web IDE? All unsaved changes will be lost.'),
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/handle_tracking_event.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/handle_tracking_event.js
index 615dad02386..5e3e5bfe4c1 100644
--- a/app/assets/javascripts/ide/lib/gitlab_web_ide/handle_tracking_event.js
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/handle_tracking_event.js
@@ -8,7 +8,7 @@ export const handleTracking = ({ name, data }) => {
if (data && Object.keys(data).length) {
Tracking.event(undefined, snakeCaseEventName, {
/* See GitLab snowplow schema for a definition of the extra field
- * https://docs.gitlab.com/ee/development/snowplow/schemas.html#gitlab_standard.
+ * https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_standard/jsonschema/1-0-9.
*/
extra: convertObjectPropsToSnakeCase(data, {
deep: true,
diff --git a/app/assets/javascripts/import/constants.js b/app/assets/javascripts/import/constants.js
index b9814b5ca60..ddf69a8fcdf 100644
--- a/app/assets/javascripts/import/constants.js
+++ b/app/assets/javascripts/import/constants.js
@@ -8,7 +8,7 @@ const STATISTIC_ITEMS = {
issue_event: __('Issue events'),
label: __('Labels'),
lfs_object: __('LFS objects'),
- merge_request_attachment: s__('GithubImporter|Merge request links'),
+ merge_request_attachment: s__('GithubImporter|PR attachments'),
milestone: __('Milestones'),
note: __('Notes'),
note_attachment: s__('GithubImporter|Note links'),
@@ -17,7 +17,7 @@ const STATISTIC_ITEMS = {
pull_request: s__('GithubImporter|Pull requests'),
pull_request_merged_by: s__('GithubImporter|PR mergers'),
pull_request_review: s__('GithubImporter|PR reviews'),
- pull_request_review_request: s__('GithubImporter|PR reviews'),
+ pull_request_review_request: s__('GithubImporter|PR reviewers'),
release: __('Releases'),
release_attachment: s__('GithubImporter|Release links'),
};
diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue
deleted file mode 100644
index 68bdcf7ef90..00000000000
--- a/app/assets/javascripts/import_entities/components/group_dropdown.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<script>
-import { GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
-import { debounce } from 'lodash';
-
-import { s__ } from '~/locale';
-import { createAlert } from '~/alert';
-import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
-import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-
-// This is added outside the component as each dropdown on the page triggers a query,
-// so if multiple queries fail, we only show 1 error.
-const reportNamespaceLoadError = debounce(
- () =>
- createAlert({
- message: s__('ImportProjects|Requesting namespaces failed'),
- }),
- DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
-);
-
-export default {
- components: {
- GlDropdown,
- GlSearchBoxByType,
- },
- inheritAttrs: false,
- data() {
- return { searchTerm: '' };
- },
- apollo: {
- namespaces: {
- query: searchNamespacesWhereUserCanImportProjectsQuery,
- variables() {
- return {
- search: this.searchTerm,
- };
- },
- skip() {
- const hasNotEnoughSearchCharacters =
- this.searchTerm.length > 0 && this.searchTerm.length < MINIMUM_SEARCH_LENGTH;
- return hasNotEnoughSearchCharacters;
- },
- update(data) {
- return data.currentUser.groups.nodes;
- },
- error: reportNamespaceLoadError,
- debounce: DEBOUNCE_DELAY,
- },
- },
- computed: {
- filteredNamespaces() {
- return (this.namespaces ?? []).filter((ns) =>
- ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()),
- );
- },
- },
-};
-</script>
-<template>
- <gl-dropdown
- toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- class="gl-h-7 gl-flex-fill-1"
- data-qa-selector="target_namespace_selector_dropdown"
- v-bind="$attrs"
- >
- <template #header>
- <gl-search-box-by-type v-model.trim="searchTerm" />
- </template>
- <slot :namespaces="filteredNamespaces"></slot>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/import_entities/components/import_target_dropdown.vue b/app/assets/javascripts/import_entities/components/import_target_dropdown.vue
index b18a106608a..47c030bf1fc 100644
--- a/app/assets/javascripts/import_entities/components/import_target_dropdown.vue
+++ b/app/assets/javascripts/import_entities/components/import_target_dropdown.vue
@@ -26,13 +26,19 @@ export default {
},
props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
selected: {
type: String,
required: true,
},
userNamespace: {
type: String,
- required: true,
+ required: false,
+ default: undefined,
},
},
@@ -66,6 +72,10 @@ export default {
},
computed: {
+ isProject() {
+ return Boolean(this.userNamespace);
+ },
+
filteredNamespaces() {
return (this.namespaces ?? []).filter((ns) =>
ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()),
@@ -78,14 +88,33 @@ export default {
items() {
return [
- {
- text: __('Users'),
- options: [{ text: this.userNamespace, value: this.userNamespace }],
- },
+ this.isProject
+ ? {
+ text: __('Users'),
+ options: [
+ {
+ text: this.userNamespace,
+ value: this.userNamespace,
+ },
+ ],
+ }
+ : {
+ text: __('Parent'),
+ textSrOnly: true,
+ options: [
+ {
+ text: s__('BulkImport|No parent'),
+ value: '',
+ },
+ ],
+ },
{
text: __('Groups'),
options: this.filteredNamespaces.map((namespace) => {
- return { text: namespace.fullPath, value: namespace.fullPath };
+ return {
+ text: namespace.fullPath,
+ value: namespace.fullPath,
+ };
}),
},
];
@@ -94,7 +123,15 @@ export default {
methods: {
onSelect(value) {
- this.$emit('select', value);
+ if (this.isProject) {
+ this.$emit('select', value);
+ } else if (value === '') {
+ this.$emit('select', { fullPath: '', id: null });
+ } else {
+ const { fullPath, id } = this.filteredNamespaces.find((ns) => ns.fullPath === value);
+
+ this.$emit('select', { fullPath, id });
+ }
},
onSearch(value) {
@@ -107,12 +144,13 @@ export default {
<template>
<gl-collapsible-listbox
:items="items"
+ :disabled="disabled"
:selected="selected"
:toggle-text="toggleText"
searchable
fluid-width
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- data-qa-selector="target_namespace_selector_dropdown"
+ data-testid="target-namespace-dropdown"
@select="onSelect"
@search="onSearch"
/>
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 cd07e9fbdd9..24197c680eb 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
@@ -393,7 +393,7 @@ export default {
}
},
- importGroup({ group, extraArgs, index }) {
+ async importGroup({ group, extraArgs, index }) {
if (group.flags.isFinished && !this.reimportRequests.includes(group.id)) {
this.validateImportTarget(group.importTarget);
this.reimportRequests.push(group.id);
@@ -402,7 +402,7 @@ export default {
});
} else {
this.reimportRequests = this.reimportRequests.filter((id) => id !== group.id);
- this.requestGroupsImport([
+ await this.requestGroupsImport([
{
sourceGroupId: group.id,
targetNamespace: group.importTarget.targetNamespace.fullPath,
@@ -410,6 +410,16 @@ export default {
...extraArgs,
},
]);
+
+ const updatedGroup = this.groups?.find((g) => g.id === group.id);
+
+ if (
+ updatedGroup.progress &&
+ updatedGroup.progress.status === STATUSES.FAILED &&
+ updatedGroup.progress.message
+ ) {
+ this.reimportRequests.push(group.id);
+ }
}
},
@@ -427,6 +437,7 @@ export default {
},
setPageSize(size) {
+ this.page = 1;
this.perPage = size;
},
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
index 807b084fefb..b4484c89b9f 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
@@ -1,20 +1,12 @@
<script>
-import {
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlFormInput,
-} from '@gitlab/ui';
-import { s__ } from '~/locale';
-import ImportGroupDropdown from '../../components/group_dropdown.vue';
+import { GlFormInput } from '@gitlab/ui';
+import ImportTargetDropdown from '../../components/import_target_dropdown.vue';
+
import { getInvalidNameValidationMessage } from '../utils';
export default {
components: {
- ImportGroupDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
+ ImportTargetDropdown,
GlFormInput,
},
props: {
@@ -25,8 +17,8 @@ export default {
},
computed: {
- fullPath() {
- return this.group.importTarget.targetNamespace.fullPath || s__('BulkImport|No parent');
+ selectedImportTarget() {
+ return this.group.importTarget.targetNamespace.fullPath || '';
},
validationMessage() {
return (
@@ -47,6 +39,10 @@ export default {
focusNewName() {
this.$refs.newName.$el.focus();
},
+
+ onImportTargetSelect(namespace) {
+ this.$emit('update-target-namespace', namespace);
+ },
},
};
</script>
@@ -54,34 +50,12 @@ export default {
<template>
<div>
<div class="gl-display-flex gl-align-items-stretch">
- <import-group-dropdown
- #default="{ namespaces }"
- :text="fullPath"
+ <import-target-dropdown
+ :selected="selectedImportTarget"
:disabled="!isPathSelectionAvailable"
- toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- class="gl-h-7 gl-flex-grow-1"
- data-qa-selector="target_namespace_selector_dropdown"
- data-testid="target-namespace-selector"
- >
- <gl-dropdown-item @click="$emit('update-target-namespace', { fullPath: '', id: null })">{{
- s__('BulkImport|No parent')
- }}</gl-dropdown-item>
- <template v-if="namespaces.length">
- <gl-dropdown-divider />
- <gl-dropdown-section-header>
- {{ s__('BulkImport|Existing groups') }}
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="ns in namespaces"
- :key="ns.fullPath"
- data-qa-selector="target_group_dropdown_item"
- :data-qa-group-name="ns.fullPath"
- @click="$emit('update-target-namespace', ns)"
- >
- {{ ns.fullPath }}
- </gl-dropdown-item>
- </template>
- </import-group-dropdown>
+ @select="onImportTargetSelect"
+ />
+
<div
class="gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-bg-gray-10"
:class="{
@@ -100,6 +74,7 @@ export default {
'gl-inset-border-1-gray-100!': !isPathSelectionAvailable,
}"
debounce="500"
+ data-testid="target-namespace-input"
:disabled="!isPathSelectionAvailable"
:value="group.importTarget.newName"
:aria-label="__('New name')"
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 782f417a989..0e1afebbe2b 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -500,6 +500,7 @@ export default {
<gl-empty-state
:title="emptyStateData.title"
:svg-path="emptyListSvgPath"
+ :svg-height="null"
:description="emptyStateData.description"
:primary-button-link="emptyStateData.btnLink"
:primary-button-text="emptyStateData.btnText"
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index e803e11bf6d..12f5fa21137 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -59,6 +59,8 @@ export const integrationTriggerEvents = {
DEPLOYMENT: 'deployment_events',
ALERT: 'alert_events',
INCIDENT: 'incident_events',
+ GROUP_MENTION: 'group_mention_events',
+ GROUP_CONFIDENTIAL_MENTION: 'group_confidential_mention_events',
};
export const integrationTriggerEventTitles = {
@@ -88,6 +90,12 @@ export const integrationTriggerEventTitles = {
[integrationTriggerEvents.INCIDENT]: s__(
'IntegrationEvents|An incident is created, closed, or reopened',
),
+ [integrationTriggerEvents.GROUP_MENTION]: s__(
+ 'IntegrationEvents|A group is mentioned in a public context',
+ ),
+ [integrationTriggerEvents.GROUP_CONFIDENTIAL_MENTION]: s__(
+ 'IntegrationEvents|A group is mentioned in a confidential context',
+ ),
};
export const billingPlans = {
diff --git a/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue b/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue
index bcb199853bd..edfb0af5bbe 100644
--- a/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue
+++ b/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue
@@ -101,7 +101,7 @@ export default {
@project-selected="selectProject"
/>
- <div class="gl-display-flex gl-justify-content-end">
+ <div class="gl-display-flex gl-justify-content-end gl-mt-3">
<gl-button
category="primary"
variant="confirm"
diff --git a/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue b/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue
index 26d191cd0bf..7c5287c69d6 100644
--- a/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue
+++ b/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue
@@ -1,13 +1,13 @@
<script>
-import { GlDropdown } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlAvatarLabeled } from '@gitlab/ui';
import { __ } from '~/locale';
-
-import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
+import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
components: {
- GlDropdown,
- ProjectListItem,
+ GlCollapsibleListbox,
+ GlAvatarLabeled,
},
props: {
projectDropdownText: {
@@ -26,30 +26,61 @@ export default {
default: null,
},
},
+ data() {
+ return {
+ selected: this.selectedProject,
+ };
+ },
computed: {
dropdownText() {
return this.selectedProject
? this.selectedProject.name_with_namespace
: this.projectDropdownText;
},
+ items() {
+ const items = this.projects.map((project) => {
+ return {
+ value: project.id,
+ ...project,
+ };
+ });
+
+ return convertObjectPropsToCamelCase(items, { deep: true });
+ },
},
methods: {
- onClick(project) {
- this.$emit('project-selected', project);
- this.$refs.dropdown.hide(true);
+ getEntityId(project) {
+ return isGid(project.id) ? getIdFromGraphQLId(project.id) : project.id;
+ },
+ selectProject(projectId) {
+ this.$emit(
+ 'project-selected',
+ this.projects.find((project) => project.id === projectId),
+ );
},
},
};
</script>
<template>
- <gl-dropdown ref="dropdown" block :text="dropdownText" menu-class="gl-w-full!">
- <project-list-item
- v-for="project in projects"
- :key="project.id"
- :project="project"
- :selected="false"
- @click="onClick(project)"
- />
- </gl-dropdown>
+ <gl-collapsible-listbox
+ v-model="selected"
+ block
+ fluid-width
+ is-check-centered
+ :toggle-text="dropdownText"
+ :items="items"
+ @select="selectProject"
+ >
+ <template #list-item="{ item }">
+ <gl-avatar-labeled
+ :label="item.nameWithNamespace"
+ :entity-name="item.nameWithNamespace"
+ :entity-id="getEntityId(item)"
+ shape="rect"
+ :size="32"
+ :src="item.avatarUrl"
+ />
+ </template>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
index 91dbd86418c..4b492e48095 100644
--- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
@@ -16,7 +16,7 @@ import GroupSelect from './group_select.vue';
import InviteGroupNotification from './invite_group_notification.vue';
export default {
- name: 'InviteMembersModal',
+ name: 'InviteGroupsModal',
components: {
GroupSelect,
InviteModalBase,
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 e9d7acdc913..509efd31dcd 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,12 +1,14 @@
<script>
import { GlAlert, GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
import { partition, isString, uniqueId, isEmpty } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue';
import Api from '~/api';
import Tracking from '~/tracking';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { n__, sprintf } from '~/locale';
import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
import {
USERS_FILTER_ALL,
MEMBER_MODAL_LABELS,
@@ -37,6 +39,9 @@ export default {
ActiveTrialNotification: () =>
import('ee_component/invite_members/components/active_trial_notification.vue'),
},
+ directives: {
+ SafeHtml,
+ },
mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })],
props: {
id: {
@@ -262,8 +267,9 @@ export default {
} else {
this.onInviteSuccess();
}
- } catch (e) {
- this.showInvalidFeedbackMessage(e);
+ } catch (error) {
+ captureException({ error, component: this.$options.name });
+ this.showInvalidFeedbackMessage(error);
} finally {
this.isLoading = false;
}
@@ -391,7 +397,8 @@ export default {
:key="error.member"
data-testid="errors-limited-item"
>
- <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
+ <strong>{{ error.displayedMemberName }}:</strong>
+ <span v-safe-html="error.message"></span>
</li>
</ul>
<template v-if="shouldErrorsSectionExpand">
@@ -402,7 +409,8 @@ export default {
:key="error.member"
data-testid="errors-expanded-item"
>
- <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
+ <strong>{{ error.displayedMemberName }}:</strong>
+ <span v-safe-html="error.message"></span>
</li>
</ul>
</gl-collapse>
diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
index 5a891e23faf..18d22395104 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -253,7 +253,6 @@ export default {
<gl-modal
ref="modal"
:modal-id="modalId"
- data-qa-selector="invite_members_modal_content"
data-testid="invite-modal"
size="sm"
dialog-class="gl-mx-5"
diff --git a/app/assets/javascripts/issuable/components/hidden_badge.vue b/app/assets/javascripts/issuable/components/hidden_badge.vue
new file mode 100644
index 00000000000..a80dc2f62d4
--- /dev/null
+++ b/app/assets/javascripts/issuable/components/hidden_badge.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { issuableTypeText } from '~/issues/constants';
+import { __, sprintf } from '~/locale';
+
+export default {
+ components: {
+ GlBadge,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ issuableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(__('This %{issuable} is hidden because its author has been banned.'), {
+ issuable: issuableTypeText[this.issuableType],
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-badge v-gl-tooltip :title="title" variant="warning">
+ <gl-icon name="spam" />
+ <span class="gl-sr-only">{{ __('Hidden') }}</span>
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/issuable/components/locked_badge.vue b/app/assets/javascripts/issuable/components/locked_badge.vue
new file mode 100644
index 00000000000..f97ac888417
--- /dev/null
+++ b/app/assets/javascripts/issuable/components/locked_badge.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { issuableTypeText } from '~/issues/constants';
+import { __, sprintf } from '~/locale';
+
+export default {
+ components: {
+ GlBadge,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ issuableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(__('This %{issuable} is locked. Only project members can comment.'), {
+ issuable: issuableTypeText[this.issuableType],
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-badge v-gl-tooltip :title="title" variant="warning">
+ <gl-icon name="lock" />
+ <span class="gl-sr-only">{{ __('Locked') }}</span>
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index ff48bfceb29..71bd301162e 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -248,7 +248,7 @@ export default {
size="small"
:disabled="removeDisabled"
class="js-issue-item-remove-button gl-mr-2"
- data-testid="remove_related_issue_button"
+ data-testid="remove-related-issue-button"
:title="__('Remove')"
:aria-label="__('Remove')"
@click="onRemoveRequest"
diff --git a/app/assets/javascripts/issuable/issuable_label_selector.js b/app/assets/javascripts/issuable/issuable_label_selector.js
index fc6d850c341..804f7384732 100644
--- a/app/assets/javascripts/issuable/issuable_label_selector.js
+++ b/app/assets/javascripts/issuable/issuable_label_selector.js
@@ -45,7 +45,7 @@ export default () => {
labelsManagePath,
variant: VARIANT_EMBEDDED,
workspaceType: WORKSPACE_PROJECT,
- toggleAttrs: { 'data-testid': 'issuable_label_dropdown' },
+ toggleAttrs: { 'data-testid': 'issuable-label-dropdown' },
},
render(createElement) {
return createElement(IssuableLabelSelector);
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index 80344efc44c..3d8017e6e07 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -28,7 +28,7 @@ export const issuableStatusText = {
[STATUS_LOCKED]: __('Open'),
};
-export const IssuableTypeText = {
+export const issuableTypeText = {
[TYPE_ISSUE]: __('issue'),
[TYPE_EPIC]: __('epic'),
[TYPE_MERGE_REQUEST]: __('merge request'),
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index 3bd28c50800..eea5207801c 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -17,17 +17,6 @@ import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initWorkItemLinks from '~/work_items/components/work_item_links';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
-import FilteredSearchServiceDesk from './filtered_search_service_desk';
-
-export function initFilteredSearchServiceDesk() {
- if (document.querySelector('.filtered-search')) {
- const supportBotData = JSON.parse(
- document.querySelector('.js-service-desk-issues').dataset.supportBot,
- );
- const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
- filteredSearchManager.setup();
- }
-}
export function initForm() {
new IssuableForm($('.issue-form')); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
index 3c58843bcbc..a9ad2db5dd3 100644
--- a/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
+++ b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
@@ -29,6 +29,7 @@ export default {
:title="$options.i18n.noSearchResultsTitle"
:svg-path="emptyStateSvgPath"
:svg-height="150"
+ data-testid="issuable-empty-state"
>
<template #actions>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
@@ -42,6 +43,8 @@ export default {
:description="$options.i18n.noOpenIssuesDescription"
:title="$options.i18n.noOpenIssuesTitle"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
+ data-testid="issuable-empty-state"
>
<template #actions>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
@@ -55,5 +58,6 @@ export default {
:title="$options.i18n.noClosedIssuesTitle"
:svg-path="emptyStateSvgPath"
:svg-height="150"
+ data-testid="issuable-empty-state"
/>
</template>
diff --git a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
index 3d62ea07f59..6741b39d5ef 100644
--- a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
+++ b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
@@ -61,6 +61,7 @@ export default {
:title="$options.i18n.noIssuesTitle"
:svg-path="emptyStateSvgPath"
:svg-height="150"
+ data-testid="issuable-empty-state"
>
<template #description>
<gl-link :href="$options.issuesHelpPagePath">
@@ -71,16 +72,26 @@ export default {
</p>
</template>
<template #actions>
- <gl-button v-if="canCreateProjects" :href="newProjectPath" variant="confirm">
+ <gl-button
+ v-if="canCreateProjects"
+ :href="newProjectPath"
+ variant="confirm"
+ class="gl-mx-2 gl-mb-3"
+ >
{{ $options.i18n.newProjectLabel }}
</gl-button>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ <gl-button
+ v-if="showNewIssueLink"
+ :href="newIssuePath"
+ variant="confirm"
+ class="gl-mx-2 gl-mb-3"
+ >
{{ $options.i18n.newIssueLabel }}
</gl-button>
<gl-disclosure-dropdown
v-if="showCsvButtons"
- class="gl-w-full gl-sm-w-auto gl-sm-mr-3"
+ class="gl-mx-2 gl-mb-3"
:toggle-text="$options.i18n.importIssues"
data-testid="import-issues-dropdown"
>
@@ -92,7 +103,7 @@ export default {
<new-resource-dropdown
v-if="showNewIssueDropdown"
- class="gl-align-self-center"
+ class="gl-align-self-center gl-mx-2 gl-mb-3"
:query="$options.searchProjectsQuery"
:query-variables="newIssueDropdownQueryVariables"
:extract-projects="extractProjects"
@@ -120,8 +131,10 @@ export default {
v-else
:title="$options.i18n.noIssuesTitle"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
:primary-button-link="signInPath"
+ data-testid="issuable-empty-state"
>
<template #description>
<gl-link :href="$options.issuesHelpPagePath">
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 3d8ed3af816..16e687cff10 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -21,7 +21,6 @@ import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_coun
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { createAlert, VARIANT_INFO } from '~/alert';
import { TYPENAME_USER } from '~/graphql_shared/constants';
-import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
@@ -384,7 +383,8 @@ export default {
dataType: 'user',
defaultUsers: [],
operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
- fetchUsers: this.fetchUsers,
+ fullPath: this.fullPath,
+ isProject: this.isProject,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`,
preloadedUsers,
},
@@ -395,7 +395,8 @@ export default {
token: UserToken,
dataType: 'user',
operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
- fetchUsers: this.fetchUsers,
+ fullPath: this.fullPath,
+ isProject: this.isProject,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
preloadedUsers,
},
@@ -634,14 +635,6 @@ export default {
fetchLatestLabels(search) {
return this.fetchLabelsWithFetchPolicy(search, fetchPolicies.NETWORK_ONLY);
},
- fetchUsers(search) {
- return this.$apollo
- .query({
- query: usersAutocompleteQuery,
- variables: { fullPath: this.fullPath, search, isProject: this.isProject },
- })
- .then(({ data }) => data[this.namespace]?.autocompleteUsers);
- },
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;
},
diff --git a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
index f3173f0e33a..3b49c0efb14 100644
--- a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
@@ -11,7 +11,6 @@ fragment IssueFragment on Issue {
moved
state
title
- titleHtml
updatedAt
closedAt
upvotes
diff --git a/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue b/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue
index ab9e70ae223..f5f06e4daef 100644
--- a/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue
+++ b/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue
@@ -55,5 +55,6 @@ export default {
:title="content.title"
:svg-path="emptyStateSvgPath"
:svg-height="content.svgHeight"
+ data-testid="issuable-empty-state"
/>
</template>
diff --git a/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue
index 9dbed2c2579..ea866dfb161 100644
--- a/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue
+++ b/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue
@@ -42,7 +42,9 @@ export default {
<gl-empty-state
:title="$options.i18n.infoBannerTitle"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
content-class="gl-max-w-80!"
+ data-testid="issues-service-desk-empty-state"
>
<template #description>
<p v-if="canSeeEmailAddress">
@@ -60,9 +62,11 @@ export default {
v-else
:title="$options.i18n.infoBannerTitle"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
:primary-button-link="signInPath"
content-class="gl-max-w-80!"
+ data-testid="issues-service-desk-empty-state"
>
<template #description>
<p>{{ $options.i18n.infoBannerUserNote }}</p>
diff --git a/app/assets/javascripts/issues/filtered_search_service_desk.js b/app/assets/javascripts/issues/service_desk/filtered_search_service_desk.js
index bec207aa439..bec207aa439 100644
--- a/app/assets/javascripts/issues/filtered_search_service_desk.js
+++ b/app/assets/javascripts/issues/service_desk/filtered_search_service_desk.js
diff --git a/app/assets/javascripts/issues/service_desk/index.js b/app/assets/javascripts/issues/service_desk/index.js
index 579cf343477..cc5f6b40a91 100644
--- a/app/assets/javascripts/issues/service_desk/index.js
+++ b/app/assets/javascripts/issues/service_desk/index.js
@@ -3,8 +3,19 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { parseBoolean } from '~/lib/utils/common_utils';
import ServiceDeskListApp from 'ee_else_ce/issues/service_desk/components/service_desk_list_app.vue';
+import FilteredSearchServiceDesk from './filtered_search_service_desk';
import { gqlClient } from './graphql';
+export function initFilteredSearchServiceDesk() {
+ if (document.querySelector('.filtered-search')) {
+ const supportBotData = JSON.parse(
+ document.querySelector('.js-service-desk-issues').dataset.supportBot,
+ );
+ const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
+ filteredSearchManager.setup();
+ }
+}
+
export async function mountServiceDeskListApp() {
const el = document.querySelector('.js-service-desk-list');
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index d59692d2a28..756585683c8 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -185,12 +185,12 @@ export default {
default: false,
},
issueId: {
- type: Number,
+ type: String,
required: false,
default: null,
},
issueIid: {
- type: Number,
+ type: String,
required: false,
default: null,
},
@@ -521,7 +521,6 @@ export default {
:project-namespace="projectNamespace"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
- :issue-id="issueId"
:issuable-type="issuableType"
@updateForm="setFormState"
/>
@@ -550,7 +549,6 @@ export default {
:issuable-type="issuableType"
:show="isStickyHeaderShowing"
:title="state.titleText"
- :title-html="state.titleHtml"
@hide="hideStickyHeader"
@show="showStickyHeader"
/>
diff --git a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
index 26e82f10c3d..23979669453 100644
--- a/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
+++ b/app/assets/javascripts/issues/show/components/delete_issue_modal.vue
@@ -35,7 +35,7 @@ export default {
return {
attributes: {
variant: 'danger',
- 'data-qa-selector': 'confirm_delete_issue_button',
+ 'data-testid': 'confirm-delete-issue-button',
},
text: this.title,
};
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index acbba216601..369aa694739 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -74,12 +74,12 @@ export default {
default: 0,
},
issueId: {
- type: Number,
+ type: String,
required: false,
default: null,
},
issueIid: {
- type: Number,
+ type: String,
required: false,
default: null,
},
@@ -362,7 +362,12 @@ export default {
},
},
update: (cache, { data: { workItemCreate } }) =>
- addHierarchyChild(cache, this.fullPath, String(this.issueIid), workItemCreate.workItem),
+ addHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.issueIid,
+ workItem: workItemCreate.workItem,
+ }),
});
const { workItem, errors } = data.workItemCreate;
@@ -392,7 +397,12 @@ export default {
mutation: deleteWorkItemMutation,
variables: { input: { id } },
update: (cache) =>
- removeHierarchyChild(cache, this.fullPath, String(this.issueIid), { id }),
+ removeHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.issueIid,
+ workItem: { id },
+ }),
});
if (data.workItemDelete.errors?.length) {
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index efe1619ed1f..10323b99665 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -2,7 +2,6 @@
<script>
import { __ } from '~/locale';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
import { ISSUE_NOTEABLE_TYPE } from '~/notes/constants';
import updateMixin from '../../mixins/update';
@@ -11,7 +10,7 @@ export default {
components: {
MarkdownEditor,
},
- mixins: [updateMixin, glFeaturesFlagMixin()],
+ mixins: [updateMixin],
props: {
value: {
type: String,
@@ -71,7 +70,6 @@ export default {
<label class="sr-only" for="issue-description">{{ __('Description') }}</label>
<markdown-editor
ref="markdownEditor"
- :enable-content-editor="Boolean(glFeatures.contentEditorOnIssues)"
class="gl-mt-3"
:value="value"
:render-markdown-path="markdownPreviewPath"
diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue
index 047bdcdcefc..c2248d66860 100644
--- a/app/assets/javascripts/issues/show/components/form.vue
+++ b/app/assets/javascripts/issues/show/components/form.vue
@@ -2,10 +2,7 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { getDraft, updateDraft, getLockVersion, clearDraft } from '~/lib/utils/autosave';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { TYPENAME_ISSUE, TYPENAME_USER } from '~/graphql_shared/constants';
import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
import EditActions from './edit_actions.vue';
import DescriptionField from './fields/description.vue';
@@ -24,7 +21,6 @@ export default {
IssuableTypeField,
LockedWarning,
},
- mixins: [glFeatureFlagMixin()],
props: {
endpoint: {
type: String,
@@ -78,11 +74,6 @@ export default {
required: false,
default: '',
},
- issueId: {
- type: Number,
- required: false,
- default: null,
- },
},
data() {
const autosaveKey = [document.location.pathname, document.location.search];
@@ -110,12 +101,6 @@ export default {
showTypeField() {
return [TYPE_INCIDENT, TYPE_ISSUE].includes(this.issuableType);
},
- resourceId() {
- return this.issueId && convertToGraphQLId(TYPENAME_ISSUE, this.issueId);
- },
- userId() {
- return convertToGraphQLId(TYPENAME_USER, gon.current_user_id);
- },
},
watch: {
formData: {
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 81e5c30a264..dee4c536afa 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -14,21 +14,16 @@ import * as Sentry from '@sentry/browser';
import { mapActions, mapGetters, mapState } from 'vuex';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
-import { STATUS_CLOSED, TYPE_ISSUE, IssuableTypeText } from '~/issues/constants';
-import {
- ISSUE_STATE_EVENT_CLOSE,
- ISSUE_STATE_EVENT_REOPEN,
- NEW_ACTIONS_POPOVER_KEY,
-} from '~/issues/show/constants';
+import { STATUS_CLOSED, TYPE_ISSUE, issuableTypeText } from '~/issues/constants';
+import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { getCookie, parseBoolean, setCookie, isLoggedIn } from '~/lib/utils/common_utils';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
import toast from '~/vue_shared/plugins/global_toast';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -70,7 +65,6 @@ export default {
GlLink,
GlModal,
AbuseCategorySelector,
- NewHeaderActionsPopover,
SidebarSubscriptionsWidget,
IssuableLockForm,
},
@@ -138,7 +132,7 @@ export default {
issueTypeText() {
const { issueType } = this;
- return IssuableTypeText[issueType] ?? issueType;
+ return issuableTypeText[issueType] ?? issueType;
},
buttonText() {
return this.isClosed
@@ -278,11 +272,6 @@ export default {
edit() {
issuesEventHub.$emit('open.form');
},
- dismissPopover() {
- if (this.isMrSidebarMoved && !parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`))) {
- setCookie(NEW_ACTIONS_POPOVER_KEY, true);
- }
- },
copyReference() {
toast(__('Reference copied'));
},
@@ -390,17 +379,6 @@ export default {
{{ $options.i18n.edit }}
</gl-button>
- <gl-button
- v-if="showToggleIssueStateButton && !glFeatures.moveCloseIntoDropdown"
- class="gl-display-none gl-sm-display-inline-flex!"
- :data-qa-selector="qaSelector"
- :loading="isToggleStateButtonLoading"
- data-testid="toggle-issue-state-button"
- @click="toggleIssueState"
- >
- {{ buttonText }}
- </gl-button>
-
<gl-dropdown
v-if="hasDesktopDropdown"
id="new-actions-header-dropdown"
@@ -415,9 +393,8 @@ export default {
data-testid="desktop-dropdown"
no-caret
right
- @shown="dismissPopover"
>
- <template v-if="showMovedSidebarOptions">
+ <template v-if="showMovedSidebarOptions && !glFeatures.notificationsTodosButtons">
<sidebar-subscriptions-widget
:iid="String(iid)"
:full-path="fullPath"
@@ -428,7 +405,7 @@ export default {
<gl-dropdown-divider />
</template>
<gl-dropdown-item
- v-if="showToggleIssueStateButton && glFeatures.moveCloseIntoDropdown"
+ v-if="showToggleIssueStateButton"
data-testid="toggle-issue-state-button"
@click="toggleIssueState"
>
@@ -492,7 +469,6 @@ export default {
</template>
</gl-dropdown>
- <new-header-actions-popover v-if="isMrSidebarMoved" :issue-type="issueType" />
<gl-modal
ref="blockedByIssuesModal"
modal-id="blocked-by-issues-modal"
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index 1905678209f..7d2b371801b 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -178,7 +178,7 @@ export default {
id="timeline-input-hours"
v-model="hourPickerInput"
data-testid="input-hours"
- size="xs"
+ width="xs"
type="number"
min="00"
max="23"
@@ -189,7 +189,7 @@ export default {
v-model="minutePickerInput"
class="gl-ml-3"
data-testid="input-minutes"
- size="xs"
+ width="xs"
type="number"
min="00"
max="59"
diff --git a/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
deleted file mode 100644
index f7a324d9f3f..00000000000
--- a/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<script>
-import { GlPopover, GlButton } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
-import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
-import { IssuableTypeText } from '~/issues/constants';
-
-export default {
- name: 'NewHeaderActionsPopover',
- i18n: {
- popoverText: s__(
- 'HeaderAction|Notifications and other %{issueType} actions have moved to this menu.',
- ),
- confirmButtonText: s__('HeaderAction|Okay!'),
- },
- components: {
- GlPopover,
- GlButton,
- },
- mixins: [glFeatureFlagMixin()],
- props: {
- issueType: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- dismissKey: NEW_ACTIONS_POPOVER_KEY,
- popoverDismissed: parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`)),
- };
- },
- computed: {
- popoverText() {
- return sprintf(this.$options.i18n.popoverText, {
- issueType: IssuableTypeText[this.issueType],
- });
- },
- showPopover() {
- return !this.popoverDismissed && this.isMrSidebarMoved;
- },
- isMrSidebarMoved() {
- return this.glFeatures.movedMrSidebar;
- },
- },
- methods: {
- dismissPopover() {
- this.popoverDismissed = true;
- setCookie(this.dismissKey, this.popoverDismissed);
- },
- },
-};
-</script>
-
-<template>
- <gl-popover
- v-if="showPopover"
- target="new-actions-header-dropdown"
- container="viewport"
- placement="left"
- :show="showPopover"
- triggers="manual"
- content="text"
- :css-classes="['gl-p-2 new-header-popover']"
- >
- <template #title>
- <div class="gl-font-base gl-font-weight-normal">
- {{ popoverText }}
- </div>
- </template>
- <gl-button
- data-testid="confirm-button"
- variant="confirm"
- type="submit"
- @click="dismissPopover"
- >{{ $options.i18n.confirmButtonText }}</gl-button
- >
- </gl-popover>
-</template>
diff --git a/app/assets/javascripts/issues/show/components/sticky_header.vue b/app/assets/javascripts/issues/show/components/sticky_header.vue
index bcf10ee92bb..738bb2c2aa0 100644
--- a/app/assets/javascripts/issues/show/components/sticky_header.vue
+++ b/app/assets/javascripts/issues/show/components/sticky_header.vue
@@ -1,12 +1,13 @@
<script>
-import { GlBadge, GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlIcon, GlIntersectionObserver, GlLink } from '@gitlab/ui';
+import HiddenBadge from '~/issuable/components/hidden_badge.vue';
+import LockedBadge from '~/issuable/components/locked_badge.vue';
import {
issuableStatusText,
STATUS_CLOSED,
TYPE_EPIC,
WORKSPACE_PROJECT,
} from '~/issues/constants';
-import SafeHtml from '~/vue_shared/directives/safe_html';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
export default {
@@ -16,10 +17,9 @@ export default {
GlBadge,
GlIcon,
GlIntersectionObserver,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- SafeHtml,
+ GlLink,
+ HiddenBadge,
+ LockedBadge,
},
props: {
isConfidential: {
@@ -54,10 +54,6 @@ export default {
type: String,
required: true,
},
- titleHtml: {
- type: String,
- required: true,
- },
},
computed: {
isClosed() {
@@ -94,35 +90,20 @@ export default {
<gl-icon :name="statusIcon" />
<span class="gl-display-none gl-sm-display-block gl-ml-2">{{ statusText }}</span>
</gl-badge>
- <span
- v-if="isLocked"
- v-gl-tooltip.bottom
- data-testid="locked"
- class="issuable-warning-icon"
- :title="__('This issue is locked. Only project members can comment.')"
- >
- <gl-icon name="lock" :aria-label="__('Locked')" />
- </span>
<confidentiality-badge
v-if="isConfidential"
:issuable-type="issuableType"
:workspace-type="$options.WORKSPACE_PROJECT"
/>
- <span
- v-if="isHidden"
- v-gl-tooltip.bottom
- :title="__('This issue is hidden because its author has been banned')"
- data-testid="hidden"
- class="issuable-warning-icon"
- >
- <gl-icon name="spam" />
- </span>
- <a
- v-safe-html="titleHtml || title"
+ <locked-badge v-if="isLocked" :issuable-type="issuableType" />
+ <hidden-badge v-if="isHidden" :issuable-type="issuableType" />
+ <gl-link
+ class="gl-font-weight-bold gl-text-black-normal gl-text-truncate"
href="#top"
- class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0 gl-text-black-normal"
+ :title="title"
>
- </a>
+ {{ title }}
+ </gl-link>
</div>
</div>
</transition>
diff --git a/app/assets/javascripts/issues/show/constants.js b/app/assets/javascripts/issues/show/constants.js
index 6320e4ef266..4d8c11f9669 100644
--- a/app/assets/javascripts/issues/show/constants.js
+++ b/app/assets/javascripts/issues/show/constants.js
@@ -17,5 +17,3 @@ export const issueState = {
issueType: undefined,
isDirty: false,
};
-
-export const NEW_ACTIONS_POPOVER_KEY = 'new-actions-popover-viewed';
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index b94f88f690e..cd5c6f4825a 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -131,8 +131,8 @@ export function initIssuableApp(store) {
isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
issuableType: issueType,
- issueId: this.getNoteableData?.id,
- issueIid: this.getNoteableData?.iid,
+ issueId: this.getNoteableData?.id.toString(),
+ issueIid: this.getNoteableData?.iid.toString(),
showTitleBorder: issueType !== TYPE_INCIDENT,
},
});
diff --git a/app/assets/javascripts/jira_connect/branches/pages/index.vue b/app/assets/javascripts/jira_connect/branches/pages/index.vue
index 3824e2350e8..3b92ace694c 100644
--- a/app/assets/javascripts/jira_connect/branches/pages/index.vue
+++ b/app/assets/javascripts/jira_connect/branches/pages/index.vue
@@ -56,6 +56,7 @@ export default {
:title="$options.i18n.I18N_NEW_BRANCH_SUCCESS_TITLE"
:description="$options.i18n.I18N_NEW_BRANCH_SUCCESS_MESSAGE"
:svg-path="successStateSvgPath"
+ :svg-height="null"
/>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index 72fd25a6230..1a10360ed30 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -37,11 +37,11 @@ export const I18N_OAUTH_FAILED_MESSAGE = s__(
export const INTEGRATIONS_DOC_LINK = helpPagePath('integration/jira/development_panel', {
anchor: 'use-the-integration',
});
-export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('integration/jira/connect-app', {
- anchor: 'connect-the-gitlab-for-jira-cloud-app-for-self-managed-instances',
+export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
+ anchor: 'set-up-oauth-authentication',
});
-export const FAILED_TO_UPDATE_DOC_LINK = helpPagePath('integration/jira/connect-app', {
- anchor: 'failed-to-update-the-gitlab-instance-for-self-managed-instances',
+export const FAILED_TO_UPDATE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
+ anchor: 'failed-to-update-the-gitlab-instance',
});
export const GITLAB_COM_BASE_PATH = 'https://gitlab.com';
diff --git a/app/assets/javascripts/jira_import/components/jira_import_progress.vue b/app/assets/javascripts/jira_import/components/jira_import_progress.vue
index 78f10decd31..2a9ce9b15ef 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_progress.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_progress.vue
@@ -56,6 +56,7 @@ export default {
<template>
<gl-empty-state
:svg-path="illustration"
+ :svg-height="null"
:title="__('Import in progress')"
:primary-button-text="__('View issues')"
:primary-button-link="issuesLink"
diff --git a/app/assets/javascripts/jira_import/components/jira_import_setup.vue b/app/assets/javascripts/jira_import/components/jira_import_setup.vue
index 285c5c815ac..58154256357 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_setup.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_setup.vue
@@ -22,6 +22,7 @@ export default {
<template>
<gl-empty-state
:svg-path="illustration"
+ :svg-height="null"
title=""
:description="__('You will first need to set up Jira Integration to use this feature.')"
:primary-button-text="__('Set up Jira Integration')"
diff --git a/app/assets/javascripts/labels/index.js b/app/assets/javascripts/labels/index.js
index bb3975ce61d..79f125be5b7 100644
--- a/app/assets/javascripts/labels/index.js
+++ b/app/assets/javascripts/labels/index.js
@@ -120,10 +120,10 @@ export function initAdminLabels() {
const emptyState = document.querySelector('.js-admin-labels-empty-state');
function removeLabelSuccessCallback() {
- this.closest('li.label-list-item').classList.add('gl-display-none!');
+ this.closest('.js-label-list-item').classList.add('gl-display-none!');
const labelsCount = document.querySelectorAll(
- 'ul.manage-labels-list li.label-list-item:not(.gl-display-none\\!)',
+ 'ul.manage-labels-list .js-label-list-item:not(.gl-display-none\\!)',
).length;
// update labels count in UI
diff --git a/app/assets/javascripts/labels/label_manager.js b/app/assets/javascripts/labels/label_manager.js
index e3d56df53f8..e684e7f1649 100644
--- a/app/assets/javascripts/labels/label_manager.js
+++ b/app/assets/javascripts/labels/label_manager.js
@@ -68,7 +68,7 @@ export default class LabelManager {
const $detachedLabel = $label.detach();
this.toggleLabelPriorityBadge($detachedLabel, action);
- const $labelEls = $target.find('li.label-list-item');
+ const $labelEls = $target.find('.js-label-list-item');
/*
* If there is a label element in the target, we'd want to
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index 36f387205f8..4354785e585 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -170,7 +170,7 @@ export default class LazyLoader {
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
// eslint-disable-next-line no-param-reassign
- img.dataset.qa_selector = 'js_lazy_loaded_content';
+ img.dataset.testid = 'js-lazy-loaded-content';
}
}
}
diff --git a/app/assets/javascripts/lib/utils/global_alerts.js b/app/assets/javascripts/lib/utils/global_alerts.js
new file mode 100644
index 00000000000..c1e4204189e
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/global_alerts.js
@@ -0,0 +1,37 @@
+export const GLOBAL_ALERTS_SESSION_STORAGE_KEY = 'vueGlobalAlerts';
+
+/**
+ * Get global alerts from session storage
+ */
+export const getGlobalAlerts = () => {
+ return JSON.parse(sessionStorage.getItem(GLOBAL_ALERTS_SESSION_STORAGE_KEY) || '[]');
+};
+
+/**
+ * Set alerts in session storage
+ * @param {{id: String, title?: String, message: String, variant: String, dismissible?: Boolean, persistOnPages?: String[]}[]} alerts
+ */
+export const setGlobalAlerts = (alerts) => {
+ sessionStorage.setItem(
+ GLOBAL_ALERTS_SESSION_STORAGE_KEY,
+ JSON.stringify([
+ ...alerts.map(({ dismissible = true, persistOnPages = [], ...alert }) => ({
+ dismissible,
+ persistOnPages,
+ ...alert,
+ })),
+ ]),
+ );
+};
+
+/**
+ * Remove global alert by id
+ * @param {String} id
+ */
+export const removeGlobalAlertById = (id) => {
+ const existingAlerts = getGlobalAlerts();
+ sessionStorage.setItem(
+ GLOBAL_ALERTS_SESSION_STORAGE_KEY,
+ JSON.stringify(existingAlerts.filter((alert) => alert.id !== id)),
+ );
+};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index ea0520e3157..a579b010877 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,3 +1,5 @@
+import { getGlobalAlerts, setGlobalAlerts } from './global_alerts';
+
export const DASH_SCOPE = '-';
export const PATH_SEPARATOR = '/';
@@ -241,7 +243,11 @@ export function removeParams(params, url = window.location.href, skipEncoding =
return `${root}${writableQuery}${writableFragment}`;
}
-export const getLocationHash = (hash = window.location.hash) => hash.split('#')[1];
+/**
+ * Returns value after the '#' in the location hash
+ * @returns Current value of the hash, undefined if not set
+ */
+export const getLocationHash = () => window.location.hash?.split('#')[1];
/**
* Returns a boolean indicating whether the URL hash contains the given string value
@@ -717,6 +723,20 @@ export function visitUrl(destination, external = false) {
}
}
+/**
+ * Navigates to a URL and display alerts.
+ *
+ * If destination is a querystring, it will be automatically transformed into a fully qualified URL.
+ * If the URL is not a safe URL (see isSafeURL implementation), this function will log an exception into Sentry.
+ *
+ * @param {*} destination - url to navigate to. This can be a fully qualified URL or a querystring.
+ * @param {{id: String, title?: String, message: String, variant: String, dismissible?: Boolean, persistOnPages?: String[]}[]} alerts - Alerts to display
+ */
+export function visitUrlWithAlerts(destination, alerts) {
+ setGlobalAlerts([...getGlobalAlerts(), ...alerts]);
+ visitUrl(destination);
+}
+
export function refreshCurrentPage() {
visitUrl(window.location.href);
}
diff --git a/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue b/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue
index 920febb0e67..68bfb99a139 100644
--- a/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue
+++ b/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue
@@ -77,7 +77,7 @@ export default {
<template>
<gl-disclosure-dropdown-item
- data-qa-selector="delete_member_dropdown_item"
+ data-testid="delete-member-dropdown-item"
@action="showRemoveMemberModal(modalData)"
>
<template #list-item>
diff --git a/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue b/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue
index 25dc4831b11..a8c97060915 100644
--- a/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue
+++ b/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue
@@ -109,7 +109,6 @@ export default {
no-caret
placement="right"
data-testid="user-action-dropdown"
- data-qa-selector="user_action_dropdown"
>
<disable-two-factor-dropdown-item
v-if="permissions.canDisableTwoFactor"
diff --git a/app/assets/javascripts/members/components/app.vue b/app/assets/javascripts/members/components/app.vue
index a70ee8fc865..06499b6d2c6 100644
--- a/app/assets/javascripts/members/components/app.vue
+++ b/app/assets/javascripts/members/components/app.vue
@@ -12,10 +12,7 @@ export default {
components: { MembersTable, FilterSortContainer, GlAlert },
provide() {
return {
- // We can't use this.namespace due to bug in vue-apollo when
- // provide is called in beforeCreate
- // See https://github.com/vuejs/vue-apollo/pull/1153 for details
- namespace: this.$options.propsData.namespace,
+ namespace: this.namespace,
};
},
props: {
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 0e5e394dd40..94773535e85 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
@@ -20,8 +20,7 @@ export default {
name: 'MembersFilteredSearchBar',
components: { FilteredSearchBar },
availableTokens: AVAILABLE_FILTERED_SEARCH_TOKENS,
- searchButtonAttributes: { 'data-qa-selector': 'search_button' },
- searchInputAttributes: { 'data-qa-selector': 'search_bar_input' },
+ searchButtonAttributes: { 'data-testid': 'search-button' },
inject: {
namespace: {},
sourceId: {},
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
index 75241d1ff26..449ad20e7ab 100644
--- a/app/assets/javascripts/members/components/members_tabs.vue
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -22,7 +22,7 @@ export const TABS = [
{
namespace: MEMBER_TYPES.group,
title: __('Groups'),
- attrs: { 'data-qa-selector': 'groups_list_tab' },
+ attrs: { 'data-testid': 'groups-list-tab' },
queryParamValue: TAB_QUERY_PARAM_VALUES.group,
},
{
@@ -112,6 +112,7 @@ export default {
<template>
<gl-tabs
v-model="selectedTabIndex"
+ content-class="gl-py-0"
sync-active-tab-with-query-params
:query-param-name="$options.ACTIVE_TAB_QUERY_PARAM_NAME"
>
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 1bc67522e82..2095f24eb84 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -93,7 +93,7 @@ function mountPipelines() {
const { mrWidgetData } = gl;
const table = new Vue({
components: {
- CommitPipelinesTable: () => {
+ MergeRequestPipelinesTable: () => {
return gon.features.mrPipelinesGraphql
? import('~/ci/merge_requests/components/pipelines_table_wrapper.vue')
: import('~/commit/pipelines/legacy_pipelines_table_wrapper.vue');
@@ -109,10 +109,10 @@ function mountPipelines() {
manualActionsLimit: 50,
mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
sourceProjectFullPath: mrWidgetData?.source_project_full_path || '',
- withFailedJobsDetails: true,
+ useFailedJobsWidget: gon.features?.ciJobFailuresInMr || false,
},
render(createElement) {
- return createElement('commit-pipelines-table', {
+ return createElement('merge-request-pipelines-table', {
props: {
endpoint: pipelineTableViewEl.dataset.endpoint,
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
@@ -347,11 +347,11 @@ export default class MergeRequestTabs {
}
// this.hideSidebar();
this.resetViewContainer();
- this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+ this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
} else if (action === 'new') {
this.expandView();
this.resetViewContainer();
- this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+ this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
} else if (this.isDiffAction(action)) {
if (!isInVueNoteablePage()) {
/*
@@ -366,7 +366,7 @@ export default class MergeRequestTabs {
}
// this.hideSidebar();
this.expandViewContainer();
- this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+ this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
// this.hideSidebar();
@@ -384,7 +384,7 @@ export default class MergeRequestTabs {
// this.showSidebar();
this.resetViewContainer();
- this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+ this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
}
renderGFM(document.querySelector('.detail-page-description'));
@@ -522,7 +522,7 @@ export default class MergeRequestTabs {
}
mountPipelinesView() {
- this.commitPipelinesTable = mountPipelines();
+ this.mergeRequestPipelinesTable = mountPipelines();
}
// load the diff tab content from the backend
diff --git a/app/assets/javascripts/merge_requests/components/compare_app.vue b/app/assets/javascripts/merge_requests/components/compare_app.vue
index c7c16e91e4c..538aa090aa8 100644
--- a/app/assets/javascripts/merge_requests/components/compare_app.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_app.vue
@@ -32,6 +32,9 @@ export default {
toggleClass: {
default: () => ({}),
},
+ compareSide: {
+ default: null,
+ },
},
props: {
currentBranch: {
@@ -116,6 +119,7 @@ export default {
:input-name="inputs.branch.name"
:default="currentBranch"
:toggle-class="toggleClass.branch"
+ :data-qa-compare-side="compareSide"
data-testid="compare-dropdown"
@selected="selectBranch"
/>
diff --git a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
index 2855d704507..20989206a51 100644
--- a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
@@ -137,7 +137,6 @@ export default {
'gl-align-items-flex-start! gl-justify-content-start! mr-compare-dropdown',
toggleClass,
]"
- data-testid="source-branch-dropdown"
@shown="fetchData"
@search="searchData"
@select="selectItem"
diff --git a/app/assets/javascripts/merge_requests/components/header_metadata.vue b/app/assets/javascripts/merge_requests/components/header_metadata.vue
deleted file mode 100644
index fce7ba385b4..00000000000
--- a/app/assets/javascripts/merge_requests/components/header_metadata.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapGetters } from 'vuex';
-import { __ } from '~/locale';
-import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants';
-import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-
-export default {
- TYPE_ISSUE,
- WORKSPACE_PROJECT,
- components: {
- GlIcon,
- ConfidentialityBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- inject: ['hidden'],
- computed: {
- ...mapGetters(['getNoteableData']),
- isLocked() {
- return this.getNoteableData.discussion_locked;
- },
- isConfidential() {
- return this.getNoteableData.confidential;
- },
- warningIconsMeta() {
- return [
- {
- iconName: 'lock',
- visible: this.isLocked,
- dataTestId: 'locked',
- tooltip: __('This merge request is locked. Only project members can comment.'),
- },
- {
- iconName: 'spam',
- visible: this.hidden,
- dataTestId: 'hidden',
- tooltip: __('This merge request is hidden because its author has been banned'),
- },
- ];
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-display-inline-block">
- <confidentiality-badge
- v-if="isConfidential"
- class="gl-mr-3"
- :issuable-type="$options.TYPE_ISSUE"
- :workspace-type="$options.WORKSPACE_PROJECT"
- />
- <template v-for="meta in warningIconsMeta">
- <div
- v-if="meta.visible"
- :key="meta.iconName"
- v-gl-tooltip.bottom
- :data-testid="meta.dataTestId"
- :title="meta.tooltip || null"
- class="issuable-warning-icon gl-mr-3 gl-mt-2 gl-display-flex gl-justify-content-center gl-align-items-center"
- >
- <gl-icon :name="meta.iconName" class="icon" />
- </div>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/merge_requests/components/merge_request_header.vue b/app/assets/javascripts/merge_requests/components/merge_request_header.vue
new file mode 100644
index 00000000000..b2e7245bd88
--- /dev/null
+++ b/app/assets/javascripts/merge_requests/components/merge_request_header.vue
@@ -0,0 +1,113 @@
+<script>
+import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import { mapGetters } from 'vuex';
+import HiddenBadge from '~/issuable/components/hidden_badge.vue';
+import LockedBadge from '~/issuable/components/locked_badge.vue';
+import StatusBadge from '~/issuable/components/status_badge.vue';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST, WORKSPACE_PROJECT } from '~/issues/constants';
+import { fetchPolicies } from '~/lib/graphql';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+
+export const badgeState = Vue.observable({
+ state: '',
+ updateStatus: null,
+});
+
+export default {
+ TYPE_ISSUE,
+ TYPE_MERGE_REQUEST,
+ WORKSPACE_PROJECT,
+ components: {
+ ConfidentialityBadge,
+ LockedBadge,
+ HiddenBadge,
+ StatusBadge,
+ },
+ inject: {
+ query: { default: null },
+ projectPath: { default: null },
+ hidden: { default: false },
+ iid: { default: null },
+ },
+ props: {
+ initialState: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ if (!this.iid) {
+ return {
+ state: this.initialState,
+ };
+ }
+
+ if (!badgeState.state && this.initialState) {
+ badgeState.state = this.initialState;
+ }
+
+ return badgeState;
+ },
+ computed: {
+ ...mapGetters(['getNoteableData']),
+ isLocked() {
+ return this.getNoteableData.discussion_locked;
+ },
+ isConfidential() {
+ return this.getNoteableData.confidential;
+ },
+ },
+ created() {
+ if (!badgeState.updateStatus) {
+ badgeState.updateStatus = this.fetchState;
+ }
+ },
+ beforeDestroy() {
+ if (badgeState.updateStatus && this.query) {
+ badgeState.updateStatus = null;
+ }
+ },
+ methods: {
+ async fetchState() {
+ const { data } = await this.$apollo.query({
+ query: this.query,
+ variables: {
+ projectPath: this.projectPath,
+ iid: this.iid,
+ },
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ });
+
+ badgeState.state = data?.workspace?.issuable?.state;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-display-contents">
+ <status-badge
+ class="gl-align-self-center gl-mr-2"
+ :issuable-type="$options.TYPE_MERGE_REQUEST"
+ :state="state"
+ />
+ <confidentiality-badge
+ v-if="isConfidential"
+ class="gl-align-self-center gl-mr-2"
+ :issuable-type="$options.TYPE_ISSUE"
+ :workspace-type="$options.WORKSPACE_PROJECT"
+ />
+ <locked-badge
+ v-if="isLocked"
+ class="gl-align-self-center gl-mr-2"
+ :issuable-type="$options.TYPE_MERGE_REQUEST"
+ />
+ <hidden-badge
+ v-if="hidden"
+ class="gl-align-self-center gl-mr-2"
+ :issuable-type="$options.TYPE_MERGE_REQUEST"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue b/app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue
deleted file mode 100644
index 3d5478757a8..00000000000
--- a/app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<script>
-import Vue from 'vue';
-import { fetchPolicies } from '~/lib/graphql';
-import StatusBadge from '~/issuable/components/status_badge.vue';
-
-export const badgeState = Vue.observable({
- state: '',
- updateStatus: null,
-});
-
-export default {
- components: {
- StatusBadge,
- },
- inject: {
- query: { default: null },
- projectPath: { default: null },
- iid: { default: null },
- },
- props: {
- initialState: {
- type: String,
- required: false,
- default: null,
- },
- issuableType: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- if (!this.iid) {
- return {
- state: this.initialState,
- };
- }
-
- if (!badgeState.state && this.initialState) {
- badgeState.state = this.initialState;
- }
-
- return badgeState;
- },
- created() {
- if (!badgeState.updateStatus) {
- badgeState.updateStatus = this.fetchState;
- }
- },
- beforeDestroy() {
- if (badgeState.updateStatus && this.query) {
- badgeState.updateStatus = null;
- }
- },
- methods: {
- async fetchState() {
- const { data } = await this.$apollo.query({
- query: this.query,
- variables: {
- projectPath: this.projectPath,
- iid: this.iid,
- },
- fetchPolicy: fetchPolicies.NO_CACHE,
- });
-
- badgeState.state = data?.workspace?.issuable?.state;
- },
- },
-};
-</script>
-
-<template>
- <status-badge class="gl-align-self-center gl-mr-3" :issuable-type="issuableType" :state="state" />
-</template>
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index c1e88a901c4..e8bdb854334 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -11,8 +11,10 @@ import StatusBadge from '~/issuable/components/status_badge.vue';
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import DiscussionCounter from '~/notes/components/discussion_counter.vue';
import TodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
+import SubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import titleSubscription from '../queries/title.subscription.graphql';
+import { badgeState } from './merge_request_header.vue';
export default {
TYPE_MERGE_REQUEST,
@@ -46,6 +48,7 @@ export default {
DiscussionCounter,
StatusBadge,
TodoWidget,
+ SubscriptionsWidget,
ClipboardButton,
},
directives: {
@@ -71,6 +74,9 @@ export default {
activeTab: (state) => state.page.activeTab,
doneFetchingBatchDiscussions: (state) => state.notes.doneFetchingBatchDiscussions,
}),
+ badgeState() {
+ return badgeState;
+ },
issuableId() {
return convertToGraphQLId(TYPENAME_MERGE_REQUEST, this.getNoteableData.id);
},
@@ -80,6 +86,9 @@ export default {
isSignedIn() {
return isLoggedIn();
},
+ isNotificationsTodosButtons() {
+ return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
+ },
},
watch: {
discussionTabCounter(val) {
@@ -120,7 +129,7 @@ export default {
<status-badge
class="gl-align-self-center gl-mr-3"
:issuable-type="$options.TYPE_MERGE_REQUEST"
- :state="getNoteableData.state"
+ :state="badgeState.state"
/>
<a
v-safe-html:[$options.safeHtmlConfig]="titleHtml"
@@ -189,13 +198,23 @@ export default {
</ul>
<div class="gl-display-none gl-lg-display-flex gl-align-items-center gl-ml-auto">
<discussion-counter blocks-merge hide-options />
- <todo-widget
+ <div
v-if="isSignedIn"
- :issuable-id="issuableId"
- :issuable-iid="issuableIid"
- :full-path="projectPath"
- issuable-type="merge_request"
- />
+ :class="{ 'gl-display-flex gl-gap-3': isNotificationsTodosButtons }"
+ >
+ <todo-widget
+ :issuable-id="issuableId"
+ :issuable-iid="issuableIid"
+ :full-path="projectPath"
+ issuable-type="merge_request"
+ />
+ <subscriptions-widget
+ v-if="isNotificationsTodosButtons"
+ :iid="issuableIid"
+ :full-path="projectPath"
+ issuable-type="merge_request"
+ />
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/merge_requests/index.js b/app/assets/javascripts/merge_requests/index.js
deleted file mode 100644
index 29218eb53e0..00000000000
--- a/app/assets/javascripts/merge_requests/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import HeaderMetadata from './components/header_metadata.vue';
-
-export function mountHeaderMetadata(store) {
- const el = document.querySelector('.js-header-metadata-root');
-
- if (!el) {
- return null;
- }
-
- return new Vue({
- el,
- name: 'HeaderMetadataRoot',
- store,
- provide: { hidden: parseBoolean(el.dataset.hidden) },
- render: (createElement) => createElement(HeaderMetadata),
- });
-}
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue
index 747e92b9e85..8c7460940a0 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue
@@ -6,18 +6,12 @@ export default {
type: String,
required: true,
},
- sectionLabel: {
- type: String,
- required: false,
- default: '',
- },
},
};
</script>
<template>
<tr>
- <td class="gl-text-secondary gl-font-weight-bold">{{ sectionLabel }}</td>
<td class="gl-font-weight-bold">{{ label }}</td>
<td>
<slot></slot>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
index a68fb7d340a..43d28e3d699 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
@@ -1,7 +1,9 @@
<script>
-import { GlAvatarLabeled, GlLink } from '@gitlab/ui';
+import { GlAvatarLabeled, GlLink, GlTableLite } from '@gitlab/ui';
+import { isEmpty, maxBy, range } from 'lodash';
import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
+import { __, sprintf } from '~/locale';
import DetailRow from './components/candidate_detail_row.vue';
import {
@@ -22,6 +24,11 @@ import {
JOB_LABEL,
CI_USER_LABEL,
CI_MR_LABEL,
+ PERFORMANCE_LABEL,
+ NO_PARAMETERS_MESSAGE,
+ NO_METRICS_MESSAGE,
+ NO_METADATA_MESSAGE,
+ NO_CI_MESSAGE,
} from './translations';
export default {
@@ -32,6 +39,7 @@ export default {
DetailRow,
GlAvatarLabeled,
GlLink,
+ GlTableLite,
},
props: {
candidate: {
@@ -54,6 +62,14 @@ export default {
JOB_LABEL,
CI_USER_LABEL,
CI_MR_LABEL,
+ PARAMETERS_LABEL,
+ METRICS_LABEL,
+ METADATA_LABEL,
+ PERFORMANCE_LABEL,
+ NO_PARAMETERS_MESSAGE,
+ NO_METRICS_MESSAGE,
+ NO_METADATA_MESSAGE,
+ NO_CI_MESSAGE,
},
computed: {
info() {
@@ -62,21 +78,38 @@ export default {
ciJob() {
return Object.freeze(this.info.ci_job);
},
- sections() {
- return [
- {
- sectionName: PARAMETERS_LABEL,
- sectionValues: this.candidate.params,
- },
- {
- sectionName: METRICS_LABEL,
- sectionValues: this.candidate.metrics,
- },
- {
- sectionName: METADATA_LABEL,
- sectionValues: this.candidate.metadata,
- },
- ];
+ hasMetadata() {
+ return !isEmpty(this.candidate.metadata);
+ },
+ hasParameters() {
+ return !isEmpty(this.candidate.params);
+ },
+ hasMetrics() {
+ return !isEmpty(this.candidate.metrics);
+ },
+ metricsTableFields() {
+ const maxStep = maxBy(this.candidate.metrics, 'step').step;
+ const rowClass = 'gl-p-3!';
+
+ const cssClasses = { thClass: rowClass, tdClass: rowClass };
+
+ const fields = range(maxStep + 1).map((step) => ({
+ key: step.toString(),
+ label: sprintf(__('Step %{step}'), { step }),
+ ...cssClasses,
+ }));
+
+ return [{ key: 'name', label: __('Metric'), ...cssClasses }, ...fields];
+ },
+ metricsTableItems() {
+ const items = {};
+ this.candidate.metrics.forEach((metric) => {
+ const metricRow = items[metric.name] || { name: metric.name };
+ metricRow[metric.step] = metric.value;
+ items[metric.name] = metricRow;
+ });
+
+ return Object.values(items);
},
},
};
@@ -93,33 +126,37 @@ export default {
/>
</model-experiments-header>
- <table class="candidate-details gl-w-full">
- <tbody>
- <tr class="divider"></tr>
-
- <detail-row :label="$options.i18n.ID_LABEL" :section-label="$options.i18n.INFO_LABEL">
- {{ info.iid }}
- </detail-row>
+ <section class="gl-mb-6">
+ <table class="candidate-details">
+ <tbody>
+ <detail-row :label="$options.i18n.ID_LABEL">
+ {{ info.iid }}
+ </detail-row>
- <detail-row :label="$options.i18n.MLFLOW_ID_LABEL">{{ info.eid }}</detail-row>
+ <detail-row :label="$options.i18n.MLFLOW_ID_LABEL">{{ info.eid }}</detail-row>
- <detail-row :label="$options.i18n.STATUS_LABEL">{{ info.status }}</detail-row>
+ <detail-row :label="$options.i18n.STATUS_LABEL">{{ info.status }}</detail-row>
- <detail-row :label="$options.i18n.EXPERIMENT_LABEL">
- <gl-link :href="info.path_to_experiment">
- {{ info.experiment_name }}
- </gl-link>
- </detail-row>
+ <detail-row :label="$options.i18n.EXPERIMENT_LABEL">
+ <gl-link :href="info.path_to_experiment">
+ {{ info.experiment_name }}
+ </gl-link>
+ </detail-row>
- <detail-row v-if="info.path_to_artifact" :label="$options.i18n.ARTIFACTS_LABEL">
- <gl-link :href="info.path_to_artifact">
- {{ $options.i18n.ARTIFACTS_LABEL }}
- </gl-link>
- </detail-row>
+ <detail-row v-if="info.path_to_artifact" :label="$options.i18n.ARTIFACTS_LABEL">
+ <gl-link :href="info.path_to_artifact">
+ {{ $options.i18n.ARTIFACTS_LABEL }}
+ </gl-link>
+ </detail-row>
+ </tbody>
+ </table>
+ </section>
- <template v-if="ciJob">
- <tr class="divider"></tr>
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.CI_SECTION_LABEL }}</h4>
+ <table v-if="ciJob" class="candidate-details">
+ <tbody>
<detail-row
:label="$options.i18n.JOB_LABEL"
:section-label="$options.i18n.CI_SECTION_LABEL"
@@ -142,21 +179,53 @@ export default {
!{{ ciJob.merge_request.iid }} {{ ciJob.merge_request.title }}
</gl-link>
</detail-row>
- </template>
+ </tbody>
+ </table>
- <template v-for="{ sectionName, sectionValues } in sections">
- <tr v-if="sectionValues" :key="sectionName" class="divider"></tr>
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_CI_MESSAGE }}</div>
+ </section>
- <detail-row
- v-for="(item, index) in sectionValues"
- :key="item.name"
- :label="item.name"
- :section-label="index === 0 ? sectionName : ''"
- >
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.PARAMETERS_LABEL }}</h4>
+
+ <table v-if="hasParameters" class="candidate-details">
+ <tbody>
+ <detail-row v-for="item in candidate.params" :key="item.name" :label="item.name">
{{ item.value }}
</detail-row>
- </template>
- </tbody>
- </table>
+ </tbody>
+ </table>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_PARAMETERS_MESSAGE }}</div>
+ </section>
+
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.METADATA_LABEL }}</h4>
+
+ <table v-if="hasMetadata" class="candidate-details">
+ <tbody>
+ <detail-row v-for="item in candidate.metadata" :key="item.name" :label="item.name">
+ {{ item.value }}
+ </detail-row>
+ </tbody>
+ </table>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_METADATA_MESSAGE }}</div>
+ </section>
+
+ <section class="gl-mb-6">
+ <h4>{{ $options.i18n.PERFORMANCE_LABEL }}</h4>
+
+ <div v-if="hasMetrics" class="gl-overflow-x-auto">
+ <gl-table-lite
+ :items="metricsTableItems"
+ :fields="metricsTableFields"
+ class="gl-w-auto"
+ hover
+ />
+ </div>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_METRICS_MESSAGE }}</div>
+ </section>
</div>
</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
index fa9518f3e27..98988e1db35 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
@@ -9,13 +9,18 @@ export const EXPERIMENT_LABEL = s__('MlExperimentTracking|Experiment');
export const ARTIFACTS_LABEL = s__('MlExperimentTracking|Artifacts');
export const PARAMETERS_LABEL = s__('MlExperimentTracking|Parameters');
export const METRICS_LABEL = s__('MlExperimentTracking|Metrics');
+export const PERFORMANCE_LABEL = s__('MlExperimentTracking|Model performance');
export const METADATA_LABEL = s__('MlExperimentTracking|Metadata');
+export const NO_PARAMETERS_MESSAGE = s__('MlExperimentTracking|No logged parameters');
+export const NO_METRICS_MESSAGE = s__('MlExperimentTracking|No logged metrics');
+export const NO_METADATA_MESSAGE = s__('MlExperimentTracking|No logged metadata');
+export const NO_CI_MESSAGE = s__('MlExperimentTracking|Candidate not linked to a CI build');
export const DELETE_CANDIDATE_CONFIRMATION_MESSAGE = s__(
'MlExperimentTracking|Deleting this candidate will delete the associated parameters, metrics, and metadata.',
);
export const DELETE_CANDIDATE_PRIMARY_ACTION_LABEL = s__('MlExperimentTracking|Delete candidate');
export const DELETE_CANDIDATE_MODAL_TITLE = s__('MLExperimentTracking|Delete candidate?');
-export const CI_SECTION_LABEL = __('CI');
+export const CI_SECTION_LABEL = s__('MLExperimentTracking|CI Info');
export const JOB_LABEL = __('Job');
export const CI_USER_LABEL = s__('MlExperimentTracking|Triggered by');
export const CI_MR_LABEL = __('Merge request');
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
index b543169d501..4710735f76e 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue
@@ -72,6 +72,7 @@ export default {
:primary-button-text="$options.i18n.CREATE_NEW_LABEL"
:primary-button-link="$options.constants.CREATE_EXPERIMENT_HELP_PATH"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:description="$options.i18n.EMPTY_STATE_DESCRIPTION_LABEL"
class="gl-py-8"
/>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
index 25c06aa2f7f..28a27059b17 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue
@@ -245,6 +245,7 @@ export default {
:primary-button-text="$options.i18n.CREATE_NEW_LABEL"
:primary-button-link="$options.constants.CREATE_CANDIDATE_HELP_PATH"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:description="$options.i18n.EMPTY_STATE_DESCRIPTION_LABEL"
class="gl-py-8"
/>
diff --git a/app/assets/javascripts/ml/model_registry/apps/index.js b/app/assets/javascripts/ml/model_registry/apps/index.js
new file mode 100644
index 00000000000..f9e5f82e708
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/index.js
@@ -0,0 +1,3 @@
+import ShowMlModel from './show_ml_model.vue';
+
+export { ShowMlModel };
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
new file mode 100644
index 00000000000..d4f17c840d7
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
@@ -0,0 +1,16 @@
+<script>
+export default {
+ name: 'ShowMlModelApp',
+ components: {},
+ props: {
+ model: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>{{ model.name }}</div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue b/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
index 37e5877ec52..3770b4ec3ac 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
+++ b/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
@@ -1,17 +1,29 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
import * as translations from '~/ml/model_registry/routes/models/index/translations';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import ModelRow from './model_row.vue';
export default {
- name: 'MlExperimentsIndexApp',
+ name: 'MlModelRegistryApp',
components: {
- GlLink,
+ Pagination,
+ ModelRow,
},
props: {
models: {
type: Array,
required: true,
},
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ hasModels() {
+ return !isEmpty(this.models);
+ },
},
i18n: translations,
};
@@ -27,8 +39,11 @@ export default {
</div>
</div>
- <div v-for="model in models" :key="model.name">
- <gl-link :href="model.path"> {{ model.name }} / {{ model.version }} </gl-link>
- </div>
+ <template v-if="hasModels">
+ <model-row v-for="model in models" :key="model.name" :model="model" />
+ <pagination v-bind="pageInfo" />
+ </template>
+
+ <p v-else class="gl-text-secondary">{{ $options.i18n.NO_MODELS_LABEL }}</p>
</div>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue b/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
new file mode 100644
index 00000000000..4f91f0939a8
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { modelVersionCountMessage } from '../translations';
+
+export default {
+ name: 'MlModelRow',
+ components: {
+ GlLink,
+ },
+ props: {
+ model: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ hasVersions() {
+ return this.model.version != null;
+ },
+ },
+ modelVersionCountMessage,
+};
+</script>
+
+<template>
+ <div class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-py-3">
+ <gl-link :href="model.path" class="gl-text-body gl-font-weight-bold gl-line-height-24">
+ {{ model.name }}
+ </gl-link>
+
+ <div class="gl-text-secondary">
+ {{ $options.modelVersionCountMessage(model.version, model.versionCount) }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js b/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
index f0f45f9424e..9210d816373 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
+++ b/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
@@ -1,3 +1,16 @@
-import { s__ } from '~/locale';
+import { s__, n__, sprintf } from '~/locale';
-export const TITLE_LABEL = s__('MlExperimentTracking|Model registry');
+export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
+export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
+
+export const modelVersionCountMessage = (version, versionCount) => {
+ if (!versionCount) return s__('MlModelRegistry|No registered versions');
+
+ const message = n__(
+ 'MlModelRegistry|%{version} · No other versions',
+ 'MlModelRegistry|%{version} · %{versionCount} versions',
+ versionCount,
+ );
+
+ return sprintf(message, { version, versionCount });
+};
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index 04167518d3f..265e2a2f880 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -44,6 +44,7 @@ export default () => {
reportAbusePath: notesDataset.reportAbusePath,
newCommentTemplatePath: notesDataset.newCommentTemplatePath,
mrFilter: true,
+ newCustomEmojiPath: notesDataset.newCustomEmojiPath,
},
data() {
const noteableData = JSON.parse(notesDataset.noteableData);
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 144cfa4295b..329d6cfec00 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -13,10 +13,9 @@ import {
slugifyWithUnderscore,
} from '~/lib/utils/text_utility';
import { sprintf } from '~/locale';
-import { badgeState } from '~/merge_requests/components/merge_request_status_badge.vue';
+import { badgeState } from '~/merge_requests/components/merge_request_header.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
import * as constants from '../constants';
@@ -49,7 +48,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagsMixin(), issuableStateMixin],
+ mixins: [issuableStateMixin],
props: {
noteableType: {
type: String,
@@ -69,7 +68,7 @@ export default {
id: 'note-body',
name: 'note[note]',
class: 'js-note-text note-textarea js-gfm-input markdown-area',
- 'data-qa-selector': 'comment_field',
+ 'data-testid': 'comment-field',
},
};
},
@@ -361,7 +360,6 @@ export default {
>
<markdown-editor
ref="markdownEditor"
- :enable-content-editor="Boolean(glFeatures.contentEditorOnIssues)"
:value="note"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
diff --git a/app/assets/javascripts/notes/components/comment_type_dropdown.vue b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
index 2e4f925194f..f85b0de0c4e 100644
--- a/app/assets/javascripts/notes/components/comment_type_dropdown.vue
+++ b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
@@ -108,7 +108,7 @@ export default {
text: this.dropdownStartThreadButtonTitle,
description: this.startDiscussionDescription,
value: constants.DISCUSSION,
- qaSelector: 'discussion_menu_item',
+ testid: 'discussion-menu-item',
},
];
},
@@ -132,7 +132,6 @@ export default {
:data-track-label="trackingLabel"
data-track-action="click_button"
data-testid="comment-button"
- data-qa-selector="comment_button"
>
<gl-button variant="confirm" :disabled="disabled" @click="handleClick">
{{ commentButtonTitle }}
@@ -149,7 +148,7 @@ export default {
@select="setNoteType"
>
<template #list-item="{ item }">
- <div :data-qa-selector="item.qaSelector">
+ <div :data-testid="item.testid">
<strong>{{ item.text }}</strong>
<p class="gl-m-0">{{ item.description }}</p>
</div>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index f08c005259c..efb6fc67806 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions } from 'vuex';
import SafeHtml from '~/vue_shared/directives/safe_html';
@@ -19,6 +19,7 @@ export default {
GlSkeletonLoader,
DiffViewer,
ImageDiffOverlay,
+ GlButton,
},
directives: {
SafeHtml,
@@ -127,12 +128,12 @@ export default {
<td class="new_line diff-line-num"></td>
<td v-if="error" class="js-error-lazy-load-diff diff-loading-error-block">
{{ __('Unable to load the diff') }}
- <button
- class="gl-button btn-link btn-link-retry gl-p-0 js-toggle-lazy-diff-retry-button gl-reset-font-size!"
+ <gl-button
+ class="btn-link-retry gl-font-regular js-toggle-lazy-diff-retry-button"
@click="fetchDiff"
>
{{ __('Try again') }}
- </button>
+ </gl-button>
</td>
<td v-else class="line_content js-success-lazy-load">
<span></span>
diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue
index dcbf4a0e5d3..c68ffd73ecc 100644
--- a/app/assets/javascripts/notes/components/discussion_actions.vue
+++ b/app/assets/javascripts/notes/components/discussion_actions.vue
@@ -49,7 +49,7 @@ export default {
<template>
<div class="discussion-with-resolve-btn clearfix">
<reply-placeholder
- data-qa-selector="discussion_reply_tab"
+ data-testid="discussion-reply-tab"
:placeholder-text="__('Reply…')"
@focus="$emit('showReplyForm')"
/>
@@ -58,7 +58,6 @@ export default {
<div class="btn-group">
<resolve-discussion-button
v-if="discussion.resolvable"
- data-qa-selector="resolve_discussion_button"
data-testid="resolve-discussion-button"
:is-resolving="isResolving"
:button-title="resolveButtonTitle"
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index d8883f90eda..b392ad55fa2 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -71,6 +71,9 @@ export default {
return options;
},
+ isNotificationsTodosButtons() {
+ return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
+ },
},
methods: {
...mapActions(['setExpandDiscussions']),
@@ -92,10 +95,12 @@ export default {
class="gl-display-flex discussions-counter"
>
<div
- class="gl-display-flex gl-align-items-center gl-pl-4 gl-rounded-base gl-mr-3 gl-min-h-7"
+ class="gl-display-flex gl-align-items-center gl-pl-4 gl-rounded-base gl-min-h-7"
:class="{
'gl-bg-orange-50': blocksMerge && !allResolved,
'gl-bg-gray-50': !blocksMerge || allResolved,
+ 'gl-mr-3': !isNotificationsTodosButtons,
+ 'gl-mr-5': isNotificationsTodosButtons,
}"
data-testid="discussions-counter-text"
>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 90f7a6862f0..bf3a750cf40 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -175,7 +175,7 @@ export default {
<gl-disclosure-dropdown
id="discussion-preferences-dropdown"
class="full-width-mobile"
- data-qa-selector="discussion_preferences_dropdown"
+ data-testid="discussion-preferences-dropdown"
:toggle-text="__('Sort or filter')"
:disabled="isLoading"
placement="right"
@@ -213,7 +213,7 @@ export default {
:is-selected="filter.value === currentValue"
:class="{ 'is-active': filter.value === currentValue }"
:data-filter-type="filterType(filter.value)"
- data-qa-selector="filter_menu_item"
+ data-testid="filter-menu-item"
@action="selectFilter(filter.value)"
>
<template #list-item>
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
index d02327a37a7..bbfde7f2e0c 100644
--- a/app/assets/javascripts/notes/components/discussion_filter_note.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -26,7 +26,7 @@ export default {
<template>
<li
class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note"
- data-qa-selector="discussion_filter_container"
+ data-testid="discussion-filter-container"
>
<div
class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600"
diff --git a/app/assets/javascripts/notes/components/email_participants_warning.vue b/app/assets/javascripts/notes/components/email_participants_warning.vue
index cf9108992be..478c5847b41 100644
--- a/app/assets/javascripts/notes/components/email_participants_warning.vue
+++ b/app/assets/javascripts/notes/components/email_participants_warning.vue
@@ -1,11 +1,12 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlButton } from '@gitlab/ui';
import { toNounSeriesText } from '~/lib/utils/grammar';
import { s__, sprintf } from '~/locale';
export default {
components: {
GlSprintf,
+ GlButton,
},
props: {
emails: {
@@ -58,9 +59,9 @@ export default {
<div class="issuable-note-warning">
<gl-sprintf :message="message">
<template #andMore>
- <button type="button" class="gl-button btn-link" @click="showMoreParticipants">
+ <gl-button variant="link" class="gl-vertical-align-baseline" @click="showMoreParticipants">
{{ moreLabel }}
- </button>
+ </gl-button>
</template>
<template #emails>
<span>{{ title }}</span>
diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue
index 2c2264c36f3..78097ff1033 100644
--- a/app/assets/javascripts/notes/components/multiline_comment_form.vue
+++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue
@@ -88,7 +88,7 @@ export default {
id="comment-line-start"
:value="commentLineStart"
:options="commentLineOptions"
- size="sm"
+ width="sm"
class="gl-w-auto gl-vertical-align-baseline"
@change="updateCommentLineStart"
/>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 7f23ee70086..5a1795d7479 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -337,7 +337,7 @@ export default {
icon="pencil"
category="tertiary"
class="note-action-button js-note-edit gl-display-none gl-sm-display-block"
- data-qa-selector="note_edit_button"
+ data-testid="note-edit-button"
@click="onEdit"
/>
<gl-button
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 363383fd7ad..f8a0db93e37 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -5,7 +5,6 @@ import { mapGetters, mapActions, mapState } from 'vuex';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
import eventHub from '../event_hub';
import issuableStateMixin from '../mixins/issuable_state';
@@ -24,7 +23,7 @@ export default {
GlLink,
GlFormCheckbox,
},
- mixins: [issuableStateMixin, resolvable, glFeaturesFlagMixin()],
+ mixins: [issuableStateMixin, resolvable],
props: {
noteBody: {
type: String,
@@ -117,7 +116,7 @@ export default {
'aria-label': __('Reply to comment'),
placeholder: this.$options.i18n.bodyPlaceholder,
class: 'note-textarea js-gfm-input js-note-text markdown-area js-vue-issue-note-form',
- 'data-qa-selector': 'reply_field',
+ 'data-testid': 'reply-field',
},
};
},
@@ -202,6 +201,9 @@ export default {
isDisabled() {
return !this.updatedNoteBody.length || this.isSubmitting;
},
+ isInternalNote() {
+ return this.discussionNote.internal || this.discussion.confidential;
+ },
discussionNote() {
const discussionNote = this.discussion.id
? this.getDiscussionLastNote(this.discussion)
@@ -221,9 +223,6 @@ export default {
placeholder: { link: ['startTag', 'endTag'] },
};
},
- enableContentEditor() {
- return Boolean(this.glFeatures.contentEditorOnIssues);
- },
codeSuggestionsConfig() {
return {
canSuggest: this.canSuggest,
@@ -355,13 +354,9 @@ export default {
</div>
<div class="flash-container timeline-content"></div>
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
- <comment-field-layout
- :noteable-data="getNoteableData"
- :is-internal-note="discussionNote.internal"
- >
+ <comment-field-layout :noteable-data="getNoteableData" :is-internal-note="isInternalNote">
<markdown-editor
ref="markdownEditor"
- :enable-content-editor="enableContentEditor"
:value="updatedNoteBody"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
@@ -406,7 +401,7 @@ export default {
category="primary"
variant="confirm"
class="gl-sm-mr-3 gl-mb-3"
- data-qa-selector="start_review_button"
+ data-testid="start-review-button"
@click="handleAddToReview"
>
<template v-if="hasDrafts">{{ __('Add to review') }}</template>
@@ -416,7 +411,7 @@ export default {
:disabled="isDisabled"
category="secondary"
variant="confirm"
- data-qa-selector="comment_now_button"
+ data-testid="comment-now-button"
class="gl-sm-mr-3 gl-mb-3 js-comment-button"
@click="handleUpdate()"
>
@@ -439,7 +434,7 @@ export default {
:disabled="isDisabled"
category="primary"
variant="confirm"
- data-qa-selector="reply_comment_button"
+ data-testid="reply-comment-button"
class="gl-sm-mr-3 gl-xs-mb-3 js-vue-issue-save js-comment-button"
@click="handleUpdate()"
>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index bdf9ea2057c..c3701c01ee2 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -188,7 +188,10 @@ export default {
v-text="authorName"
></span>
</a>
- <span v-if="!isSystemNote && !emailParticipant" class="text-nowrap author-username">
+ <span
+ v-if="!isSystemNote && !emailParticipant"
+ class="text-nowrap author-username gl-text-truncate"
+ >
<a
ref="authorUsernameLink"
class="author-username-link"
@@ -205,7 +208,7 @@ export default {
</template>
<span v-else>{{ __('A deleted user') }}</span>
<span class="note-headline-light note-headline-meta">
- <span class="system-note-message" data-qa-selector="system_note_content">
+ <span class="system-note-message" data-testid="system-note-content">
<slot></slot>
</span>
<template v-if="createdAt">
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 94d5dc25b9e..e0b1f7a8c6a 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -310,7 +310,7 @@ export default {
:data-discussion-resolvable="discussion.resolvable"
:data-discussion-resolved="discussion.resolved"
class="discussion js-discussion-container"
- data-qa-selector="discussion_content"
+ data-testid="discussion-content"
>
<diff-discussion-header v-if="shouldRenderDiffs" :discussion="discussion" />
<div v-if="!shouldHideDiscussionBody" class="discussion-body">
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 9a7cc1a4d37..809b1716b91 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -421,7 +421,7 @@ export default {
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
class="note note-wrapper note-comment"
- data-qa-selector="noteable_note_container"
+ data-testid="noteable-note-container"
>
<div
v-if="showMultiLineComment"
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
index a91c825710d..ce642733396 100644
--- a/app/assets/javascripts/notes/components/notes_activity_header.vue
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -38,7 +38,11 @@ export default {
},
computed: {
showAiActions() {
- return this.resourceGlobalId && this.glFeatures.summarizeComments;
+ return (
+ this.resourceGlobalId &&
+ this.glFeatures.openaiExperimentation &&
+ this.glFeatures.summarizeNotes
+ );
},
},
};
@@ -56,7 +60,7 @@ export default {
:loading="aiLoading"
/>
<timeline-toggle v-if="showTimelineViewToggle" />
- <mr-discussion-filter v-if="mrFilter && glFeatures.mrActivityFilters" />
+ <mr-discussion-filter v-if="mrFilter" />
<discussion-filter v-else :filters="notesFilters" :selected-value="notesFilterValue" />
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index a012b4411bc..981b9324688 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -84,12 +84,7 @@ export default {
:tooltip-text="author.name"
tooltip-placement="bottom"
/>
- <gl-button
- class="gl-mr-2"
- variant="link"
- data-qa-selector="expand_replies_button"
- @click="toggle"
- >
+ <gl-button class="gl-mr-2" variant="link" data-testid="expand-replies-button" @click="toggle">
{{ n__('%d reply', '%d replies', replies.length) }}
</gl-button>
<gl-sprintf :message="$options.i18n.lastReplyBy">
@@ -111,7 +106,7 @@ export default {
v-else
class="gl-text-body! gl-text-decoration-none!"
variant="link"
- data-qa-selector="collapse_replies_button"
+ data-testid="collapse-replies-button"
@click="toggle"
>
{{ $options.i18n.collapseReplies }}
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 724b47bf44b..f9fbe6659ee 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -62,6 +62,7 @@ export default ({ editorAiActions = [] } = {}) => {
newCommentTemplatePath: notesDataset.newCommentTemplatePath,
resourceGlobalId: convertToGraphQLId(noteableData.noteableType, noteableData.id),
editorAiActions: editorAiActions.map((factory) => factory(noteableData)),
+ newCustomEmojiPath: notesDataset.newCustomEmojiPath,
},
data() {
return {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 7eb01897296..4071218d100 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -95,10 +95,7 @@ export const fetchDiscussions = (
? { params: { notes_filter: filter, persist_filter: persistFilter } }
: null;
- if (
- window.gon?.features?.mrActivityFilters &&
- getters.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
- ) {
+ if (getters.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) {
config = { params: { notes_filter: 0, persist_filter: false } };
}
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index c43430639ad..62d991c2d9e 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -2,7 +2,7 @@ import { flattenDeep, clone } from 'lodash';
import { match } from '~/diffs/utils/diff_file';
import { isInMRPage } from '~/lib/utils/common_utils';
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
-import { badgeState } from '~/merge_requests/components/merge_request_status_badge.vue';
+import { badgeState } from '~/merge_requests/components/merge_request_header.vue';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
@@ -52,10 +52,7 @@ export const discussions = (state, getters, rootState) => {
let discussionsInState = clone(state.discussions);
// NOTE: not testing bc will be removed when backend is finished.
- if (
- state.noteableData.targetType === 'merge_request' &&
- window.gon?.features?.mrActivityFilters
- ) {
+ if (state.noteableData.targetType === 'merge_request') {
discussionsInState = discussionsInState.reduce((acc, discussion) => {
if (hideActivity(state.mergeRequestFilters, discussion)) {
return acc;
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index 718001e98fe..2e976cd6230 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -140,7 +140,7 @@ function filterObjToQueryParams(filterObj) {
let value = rawValue;
if (filterName === 'durationMs') {
// converting durationMs to duration_nano
- value *= 1000;
+ value *= 1000000;
}
if (paramName && value) {
@@ -166,28 +166,80 @@ function filterObjToQueryParams(filterObj) {
*
* @returns Array<Trace> : A list of traces
*/
-async function fetchTraces(tracingUrl, filters = {}) {
- const filterParams = filterObjToQueryParams(filters);
+async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize } = {}) {
+ const params = filterObjToQueryParams(filters);
+ if (pageToken) {
+ params.append('page_token', pageToken);
+ }
+ if (pageSize) {
+ params.append('page_size', pageSize);
+ }
try {
const { data } = await axios.get(tracingUrl, {
withCredentials: true,
- params: filterParams,
+ params,
});
if (!Array.isArray(data.traces)) {
throw new Error('traces are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
}
- return data.traces;
+ return data;
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
+}
+
+async function fetchServices(servicesUrl) {
+ try {
+ const { data } = await axios.get(servicesUrl, {
+ withCredentials: true,
+ });
+
+ if (!Array.isArray(data.services)) {
+ throw new Error('failed to fetch services. invalid response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+
+ return data.services;
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
+}
+
+async function fetchOperations(operationsUrl, serviceName) {
+ try {
+ if (!serviceName) {
+ throw new Error('fetchOperations() - serviceName is required.');
+ }
+ if (!operationsUrl.includes('$SERVICE_NAME$')) {
+ throw new Error('fetchOperations() - operationsUrl must contain $SERVICE_NAME$');
+ }
+ const url = operationsUrl.replace('$SERVICE_NAME$', serviceName);
+ const { data } = await axios.get(url, {
+ withCredentials: true,
+ });
+
+ if (!Array.isArray(data.operations)) {
+ throw new Error('failed to fetch operations. invalid response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+
+ return data.operations;
} catch (e) {
return reportErrorAndThrow(e);
}
}
-export function buildClient({ provisioningUrl, tracingUrl }) {
+export function buildClient({ provisioningUrl, tracingUrl, servicesUrl, operationsUrl } = {}) {
+ if (!provisioningUrl || !tracingUrl || !servicesUrl || !operationsUrl) {
+ throw new Error(
+ 'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
+ );
+ }
return {
enableTraces: () => enableTraces(provisioningUrl),
isTracingEnabled: () => isTracingEnabled(provisioningUrl),
fetchTraces: (filters) => fetchTraces(tracingUrl, filters),
fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
+ fetchServices: () => fetchServices(servicesUrl),
+ fetchOperations: (serviceName) => fetchOperations(operationsUrl, serviceName),
};
}
diff --git a/app/assets/javascripts/observability/components/observability_app.vue b/app/assets/javascripts/observability/components/observability_app.vue
deleted file mode 100644
index 36cbe715149..00000000000
--- a/app/assets/javascripts/observability/components/observability_app.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<script>
-import { darkModeEnabled } from '~/lib/utils/color_utils';
-import { setUrlParams } from '~/lib/utils/url_utility';
-
-import { MESSAGE_EVENT_TYPE, FULL_APP_DIMENSIONS } from '../constants';
-import ObservabilitySkeleton from './skeleton/index.vue';
-
-export default {
- components: {
- ObservabilitySkeleton,
- },
- props: {
- observabilityIframeSrc: {
- type: String,
- required: true,
- },
- inlineEmbed: {
- type: Boolean,
- required: false,
- default: false,
- },
- skeletonVariant: {
- type: String,
- required: false,
- default: 'dashboards',
- },
- height: {
- type: String,
- required: false,
- default: FULL_APP_DIMENSIONS.HEIGHT,
- },
- width: {
- type: String,
- required: false,
- default: FULL_APP_DIMENSIONS.WIDTH,
- },
- },
- computed: {
- iframeSrcWithParams() {
- return `${setUrlParams(
- { theme: darkModeEnabled() ? 'dark' : 'light', username: gon?.current_username },
- this.observabilityIframeSrc,
- )}${this.inlineEmbed ? '&kiosk=inline-embed' : ''}`;
- },
- },
- mounted() {
- window.addEventListener('message', this.messageHandler);
- },
- destroyed() {
- window.removeEventListener('message', this.messageHandler);
- },
- methods: {
- messageHandler(e) {
- const isExpectedOrigin = e.origin === new URL(this.observabilityIframeSrc)?.origin;
- if (!isExpectedOrigin) return;
-
- const {
- data: { type, payload },
- } = e;
- switch (type) {
- case MESSAGE_EVENT_TYPE.GOUI_LOADED:
- this.$refs.observabilitySkeleton.onContentLoaded();
- break;
- case MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE:
- this.$emit('route-update', payload);
- break;
- default:
- break;
- }
- },
- },
-};
-</script>
-
-<template>
- <observability-skeleton ref="observabilitySkeleton" :variant="skeletonVariant">
- <iframe
- id="observability-ui-iframe"
- data-testid="observability-ui-iframe"
- frameborder="0"
- :width="width"
- :height="height"
- :src="iframeSrcWithParams"
- sandbox="allow-same-origin allow-forms allow-scripts"
- ></iframe>
- </observability-skeleton>
-</template>
diff --git a/app/assets/javascripts/observability/components/observability_container.vue b/app/assets/javascripts/observability/components/observability_container.vue
index b7697cea299..1518c132560 100644
--- a/app/assets/javascripts/observability/components/observability_container.vue
+++ b/app/assets/javascripts/observability/components/observability_container.vue
@@ -13,11 +13,19 @@ export default {
type: String,
required: true,
},
+ provisioningUrl: {
+ type: String,
+ required: true,
+ },
tracingUrl: {
type: String,
required: true,
},
- provisioningUrl: {
+ servicesUrl: {
+ type: String,
+ required: true,
+ },
+ operationsUrl: {
type: String,
required: true,
},
@@ -58,6 +66,8 @@ export default {
this.observabilityClient = buildClient({
provisioningUrl: this.provisioningUrl,
tracingUrl: this.tracingUrl,
+ servicesUrl: this.servicesUrl,
+ operationsUrl: this.operationsUrl,
});
this.$refs.observabilitySkeleton?.onContentLoaded();
} else if (status === 'error') {
diff --git a/app/assets/javascripts/observability/components/skeleton/dashboards.vue b/app/assets/javascripts/observability/components/skeleton/dashboards.vue
deleted file mode 100644
index 887a0a9f094..00000000000
--- a/app/assets/javascripts/observability/components/skeleton/dashboards.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-export default {
- components: {
- GlSkeletonLoader,
- },
-};
-</script>
-<template>
- <gl-skeleton-loader :height="200">
- <!-- Top left -->
- <rect y="2" width="10" height="8" />
- <rect y="2" x="15" width="15" height="8" />
- <rect y="2" x="35" width="15" height="8" />
-
- <!-- Top right -->
- <rect y="2" x="354" width="10" height="8" />
- <rect y="2" x="366" width="10" height="8" />
- <rect y="2" x="378" width="10" height="8" />
- <rect y="2" x="390" width="10" height="8" />
-
- <!-- Middle header -->
- <rect y="15" width="400" height="30" rx="2" ry="2" />
-
- <!-- Dashboard container -->
- <rect y="50" width="200" height="100" rx="2" ry="2" />
- </gl-skeleton-loader>
-</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/embed.vue b/app/assets/javascripts/observability/components/skeleton/embed.vue
deleted file mode 100644
index 965beb168bf..00000000000
--- a/app/assets/javascripts/observability/components/skeleton/embed.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-export default {
- components: {
- GlSkeletonLoader,
- },
-};
-</script>
-<template>
- <gl-skeleton-loader>
- <rect y="5" width="400" height="30" rx="2" ry="2" />
- <rect y="50" width="400" height="80" rx="2" ry="2" />
- </gl-skeleton-loader>
-</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/explore.vue b/app/assets/javascripts/observability/components/skeleton/explore.vue
deleted file mode 100644
index 3f748086eef..00000000000
--- a/app/assets/javascripts/observability/components/skeleton/explore.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-export default {
- components: {
- GlSkeletonLoader,
- },
-};
-</script>
-<template>
- <gl-skeleton-loader :height="200">
- <!-- Top left -->
- <circle y="2" cx="6" cy="6" r="4" />
- <rect y="2" x="15" width="15" height="8" />
- <rect y="2" x="35" width="40" height="8" />
-
- <!-- Top right -->
-
- <rect y="2" x="263" width="13" height="8" />
- <rect y="2" x="278" width="8" height="8" />
- <rect y="2" x="288" width="50" height="8" />
- <rect y="2" x="340" width="18" height="8" />
- <rect y="2" x="360" width="30" height="8" />
-
- <rect y="15" width="400" height="30" rx="2" ry="2" />
- </gl-skeleton-loader>
-</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/index.vue b/app/assets/javascripts/observability/components/skeleton/index.vue
index d3c6892df50..c3d0a7c90b1 100644
--- a/app/assets/javascripts/observability/components/skeleton/index.vue
+++ b/app/assets/javascripts/observability/components/skeleton/index.vue
@@ -3,34 +3,20 @@
import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
import {
- SKELETON_VARIANTS_BY_ROUTE,
SKELETON_STATE,
DEFAULT_TIMERS,
- OBSERVABILITY_ROUTES,
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
- SKELETON_VARIANT_EMBED,
SKELETON_SPINNER_VARIANT,
} from '../../constants';
-import DashboardsSkeleton from './dashboards.vue';
-import ExploreSkeleton from './explore.vue';
-import ManageSkeleton from './manage.vue';
-import EmbedSkeleton from './embed.vue';
export default {
components: {
GlSkeletonLoader,
- DashboardsSkeleton,
- ExploreSkeleton,
- ManageSkeleton,
- EmbedSkeleton,
GlAlert,
GlLoadingIcon,
},
- SKELETON_VARIANTS_BY_ROUTE,
SKELETON_STATE,
- OBSERVABILITY_ROUTES,
- SKELETON_VARIANT_EMBED,
i18n: {
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
@@ -62,9 +48,6 @@ export default {
spinnerVariant() {
return this.variant === SKELETON_SPINNER_VARIANT;
},
- embedVariant() {
- return this.variant === SKELETON_VARIANT_EMBED;
- },
},
mounted() {
this.setLoadingTimeout();
@@ -118,9 +101,6 @@ export default {
showError() {
this.state = SKELETON_STATE.ERROR;
},
- isVariantByRoute(route) {
- return this.variant === SKELETON_VARIANTS_BY_ROUTE[route];
- },
},
};
</script>
@@ -128,12 +108,7 @@ export default {
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch">
<transition name="fade">
<div v-if="skeletonVisible" class="gl-px-5 gl-my-5">
- <dashboards-skeleton v-if="isVariantByRoute($options.OBSERVABILITY_ROUTES.DASHBOARDS)" />
- <explore-skeleton v-else-if="isVariantByRoute($options.OBSERVABILITY_ROUTES.EXPLORE)" />
- <manage-skeleton v-else-if="isVariantByRoute($options.OBSERVABILITY_ROUTES.MANAGE)" />
- <embed-skeleton v-else-if="embedVariant" />
- <gl-loading-icon v-else-if="spinnerVariant" size="lg" />
-
+ <gl-loading-icon v-if="spinnerVariant" size="lg" />
<gl-skeleton-loader v-else>
<rect y="2" width="10" height="8" />
<rect y="2" x="15" width="15" height="8" />
diff --git a/app/assets/javascripts/observability/components/skeleton/manage.vue b/app/assets/javascripts/observability/components/skeleton/manage.vue
deleted file mode 100644
index cf8c900fe11..00000000000
--- a/app/assets/javascripts/observability/components/skeleton/manage.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-export default {
- components: {
- GlSkeletonLoader,
- },
-};
-</script>
-<template>
- <gl-skeleton-loader :height="200">
- <!-- Top header-->
- <rect y="2" width="400" height="30" />
-
- <rect y="35" x="65" width="80" height="8" />
- <rect y="35" x="205" width="30" height="8" />
- <rect y="35" x="240" width="25" height="8" />
- <rect y="35" x="270" width="20" height="8" />
-
- <rect y="55" x="65" width="100" height="8" />
- <rect y="55" x="225" width="65" height="8" />
-
- <rect y="65" x="65" width="225" height="200" rx="2" ry="2" />
- </gl-skeleton-loader>
-</template>
diff --git a/app/assets/javascripts/observability/constants.js b/app/assets/javascripts/observability/constants.js
index b0a0941779d..83eaea185e5 100644
--- a/app/assets/javascripts/observability/constants.js
+++ b/app/assets/javascripts/observability/constants.js
@@ -1,23 +1,5 @@
import { __ } from '~/locale';
-export const MESSAGE_EVENT_TYPE = Object.freeze({
- GOUI_LOADED: 'GOUI_LOADED',
- GOUI_ROUTE_UPDATE: 'GOUI_ROUTE_UPDATE',
-});
-
-export const OBSERVABILITY_ROUTES = Object.freeze({
- DASHBOARDS: 'observability/dashboards',
- EXPLORE: 'observability/explore',
- MANAGE: 'observability/manage',
-});
-
-export const SKELETON_VARIANTS_BY_ROUTE = Object.freeze({
- [OBSERVABILITY_ROUTES.DASHBOARDS]: 'dashboards',
- [OBSERVABILITY_ROUTES.EXPLORE]: 'explore',
- [OBSERVABILITY_ROUTES.MANAGE]: 'manage',
-});
-
-export const SKELETON_VARIANT_EMBED = 'embed';
export const SKELETON_SPINNER_VARIANT = 'spinner';
export const SKELETON_STATE = Object.freeze({
@@ -33,13 +15,3 @@ export const DEFAULT_TIMERS = Object.freeze({
export const TIMEOUT_ERROR_LABEL = __('Unable to load the page');
export const TIMEOUT_ERROR_MESSAGE = __('Reload the page to try again.');
-
-export const INLINE_EMBED_DIMENSIONS = Object.freeze({
- HEIGHT: '366px',
- WIDTH: '768px',
-});
-
-export const FULL_APP_DIMENSIONS = Object.freeze({
- HEIGHT: '100%',
- WIDTH: '100%',
-});
diff --git a/app/assets/javascripts/observability/index.js b/app/assets/javascripts/observability/index.js
deleted file mode 100644
index 72ff1357551..00000000000
--- a/app/assets/javascripts/observability/index.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-
-import ObservabilityApp from './components/observability_app.vue';
-import { SKELETON_VARIANTS_BY_ROUTE } from './constants';
-
-Vue.use(VueRouter);
-
-export default () => {
- const el = document.getElementById('js-observability-app');
-
- if (!el) return false;
-
- const router = new VueRouter({
- mode: 'history',
- });
-
- return new Vue({
- el,
- router,
- computed: {
- skeletonVariant() {
- const [, variant] =
- Object.entries(SKELETON_VARIANTS_BY_ROUTE).find(([path]) =>
- this.$route.path.endsWith(path),
- ) || [];
-
- return variant;
- },
- },
- methods: {
- routeUpdateHandler(payload) {
- const isNewObservabilityPath = this.$route?.query?.observability_path !== payload?.url;
-
- const shouldNotHandleMessage = !payload.url || !isNewObservabilityPath;
-
- if (shouldNotHandleMessage) {
- return;
- }
-
- // this will update the `observability_path` query param on each route change inside Observability UI
- this.$router.replace({
- name: this.$route?.pathname,
- query: { ...this.$route.query, observability_path: payload.url },
- });
- },
- },
- render(h) {
- return h(ObservabilityApp, {
- props: {
- observabilityIframeSrc: el.dataset.observabilityIframeSrc,
- skeletonVariant: this.skeletonVariant,
- },
- on: {
- 'route-update': (payload) => this.routeUpdateHandler(payload),
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/observability/mock_traces.json b/app/assets/javascripts/observability/mock_traces.json
deleted file mode 100644
index cd7dfb40af6..00000000000
--- a/app/assets/javascripts/observability/mock_traces.json
+++ /dev/null
@@ -1,107 +0,0 @@
-{
- "project_id": 123,
- "traces": [
- {
- "timestamp": "2023-08-07T15:03:32.199806Z",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "tracegentracegentracegenttracegentracegentracegent",
- "operation": "lets-golets-golets-goletslets-golets-golets-golets",
- "statusCode": "STATUS_CODE_UNSET",
- "duration_nano": 100120000,
- "spans": [
- {
- "timestamp": "2023-08-07T15:03:32.199806Z",
- "span_id": "A1FB81EB031B09E8",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "tracegentracegentracegentracegentracegentracegentracegentracegentracegentracegentracegentracegentracegen",
- "operation": "lets-golets-golets-golets-golets-golets-golets-golets-golets-golets-golets-golets-go",
- "duration_nano": 100120000,
- "parent_span_id": "",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:03:32.199871Z",
- "span_id": "9C920500FE9C85E3",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "tracegen",
- "operation": "okey-dokey",
- "duration_nano": 100055000,
- "parent_span_id": "A1FB81EB031B09E8",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:03:53.199871Z",
- "span_id": "FAKE",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "tracegen",
- "operation": "okey-dokey",
- "duration_nano": 50027500,
- "parent_span_id": "9C920500FE9C85E3",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:03:53.199871Z",
- "span_id": "FAKE-2",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-2",
- "operation": "okey-dokey",
- "duration_nano": 50027500,
- "parent_span_id": "9C920500FE9C85E3",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:04:13.199871Z",
- "span_id": "FAKE-3",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-3",
- "operation": "okey-dokey",
- "duration_nano": 30000000,
- "parent_span_id": "FAKE-2",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:04:13.199871Z",
- "span_id": "FAKE-4",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-4",
- "operation": "okey-dokey",
- "duration_nano": 25000000,
- "parent_span_id": "FAKE-3",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:04:13.199871Z",
- "span_id": "FAKE-5",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-5",
- "operation": "okey-dokey",
- "duration_nano": 10000000,
- "parent_span_id": "FAKE-4",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:04:13.199871Z",
- "span_id": "FAKE-6",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-6",
- "operation": "okey-dokey",
- "duration_nano": 10000000,
- "parent_span_id": "FAKE-5",
- "statusCode": "STATUS_CODE_UNSET"
- },
- {
- "timestamp": "2023-08-07T15:04:13.199871Z",
- "span_id": "FAKE-7",
- "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
- "service_name": "fake-service-7",
- "operation": "okey-dokey",
- "duration_nano": 5000000,
- "parent_span_id": "FAKE-6",
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 5
- }
- ],
- "totalTraces": 50
-}
diff --git a/app/assets/javascripts/organizations/index/components/app.vue b/app/assets/javascripts/organizations/index/components/app.vue
new file mode 100644
index 00000000000..c47f4ed52c5
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/app.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import organizationsQuery from '../graphql/organizations.query.graphql';
+import OrganizationsView from './organizations_view.vue';
+
+export default {
+ name: 'OrganizationsIndexApp',
+ i18n: {
+ organizations: __('Organizations'),
+ newOrganization: s__('Organization|New organization'),
+ errorMessage: s__(
+ 'Organization|An error occurred loading user organizations. Please refresh the page to try again.',
+ ),
+ },
+ components: {
+ GlButton,
+ OrganizationsView,
+ },
+ inject: ['newOrganizationUrl'],
+ data() {
+ return {
+ organizations: [],
+ };
+ },
+ apollo: {
+ organizations: {
+ query: organizationsQuery,
+ update(data) {
+ return data.currentUser.organizations.nodes;
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ showHeader() {
+ return this.loading || this.organizations.length;
+ },
+ loading() {
+ return this.$apollo.queries.organizations.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <section>
+ <div v-if="showHeader" class="gl-display-flex gl-align-items-center">
+ <h1 class="gl-my-4 gl-font-size-h-display">{{ $options.i18n.organizations }}</h1>
+ <div class="gl-ml-auto">
+ <gl-button :href="newOrganizationUrl" variant="confirm">{{
+ $options.i18n.newOrganization
+ }}</gl-button>
+ </div>
+ </div>
+ <organizations-view :organizations="organizations" :loading="loading" />
+ </section>
+</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_list.vue b/app/assets/javascripts/organizations/index/components/organizations_list.vue
new file mode 100644
index 00000000000..539a4fcfe29
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/organizations_list.vue
@@ -0,0 +1,26 @@
+<script>
+import OrganizationsListItem from './organizations_list_item.vue';
+
+export default {
+ name: 'OrganizationsList',
+ components: {
+ OrganizationsListItem,
+ },
+ props: {
+ organizations: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <ul class="gl-p-0 gl-list-style-none">
+ <organizations-list-item
+ v-for="organization in organizations"
+ :key="organization.id"
+ :organization="organization"
+ />
+ </ul>
+</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_list_item.vue b/app/assets/javascripts/organizations/index/components/organizations_list_item.vue
new file mode 100644
index 00000000000..589835874ad
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/organizations_list_item.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlAvatarLabeled, GlTruncateText } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+
+export default {
+ name: 'OrganizationsListItem',
+ components: {
+ GlAvatarLabeled,
+ GlTruncateText,
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['gl-emoji'],
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ organization: {
+ type: Object,
+ required: true,
+ },
+ },
+ avatarSize: { default: 32, md: 48 },
+ getIdFromGraphQLId,
+};
+</script>
+
+<template>
+ <li class="organization-row gl-py-3 gl-border-b gl-display-flex gl-align-items-flex-start">
+ <gl-avatar-labeled
+ :size="$options.avatarSize"
+ :src="organization.avatarUrl"
+ :entity-id="$options.getIdFromGraphQLId(organization.id)"
+ :entity-name="organization.name"
+ :label="organization.name"
+ :label-link="organization.webUrl"
+ shape="rect"
+ >
+ <gl-truncate-text
+ v-if="organization.descriptionHtml"
+ :lines="2"
+ :mobile-lines="2"
+ class="gl-mt-2"
+ >
+ <div
+ v-safe-html:[$options.safeHtmlConfig]="organization.descriptionHtml"
+ data-testid="organization-description-html"
+ class="organization-description gl-text-secondary gl-font-sm"
+ ></div>
+ </gl-truncate-text>
+ </gl-avatar-labeled>
+ </li>
+</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_view.vue b/app/assets/javascripts/organizations/index/components/organizations_view.vue
new file mode 100644
index 00000000000..9720646bca3
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/components/organizations_view.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import OrganizationsList from './organizations_list.vue';
+
+export default {
+ name: 'OrganizationsView',
+ i18n: {
+ emptyStateTitle: s__('Organization|Get started with organizations'),
+ emptyStateDescription: s__(
+ 'Organization|Create an organization to contain all of your groups and projects.',
+ ),
+ emptyStateButtonText: s__('Organization|New organization'),
+ },
+ components: {
+ GlLoadingIcon,
+ OrganizationsList,
+ GlEmptyState,
+ },
+ inject: ['newOrganizationUrl', 'organizationsEmptyStateSvgPath'],
+ props: {
+ organizations: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="loading" class="gl-mt-5" size="md" />
+ <organizations-list
+ v-else-if="organizations.length"
+ :organizations="organizations"
+ class="gl-border-t"
+ />
+ <gl-empty-state
+ v-else
+ :svg-height="144"
+ :svg-path="organizationsEmptyStateSvgPath"
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ :primary-button-link="newOrganizationUrl"
+ :primary-button-text="$options.i18n.emptyStateButtonText"
+ />
+</template>
diff --git a/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql b/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql
new file mode 100644
index 00000000000..6090e2ec789
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql
@@ -0,0 +1,14 @@
+query getCurrentUserOrganizations {
+ currentUser {
+ id
+ organizations @client {
+ nodes {
+ id
+ name
+ descriptionHtml
+ avatarUrl
+ webUrl
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/organizations/index/index.js b/app/assets/javascripts/organizations/index/index.js
new file mode 100644
index 00000000000..7cbb9c9165d
--- /dev/null
+++ b/app/assets/javascripts/organizations/index/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import resolvers from '../shared/graphql/resolvers';
+import OrganizationsIndexApp from './components/app.vue';
+
+export const initOrganizationsIndex = () => {
+ const el = document.getElementById('js-organizations-index');
+
+ if (!el) return false;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ const { newOrganizationUrl, organizationsEmptyStateSvgPath } = convertObjectPropsToCamelCase(
+ el.dataset,
+ );
+
+ return new Vue({
+ el,
+ name: 'OrganizationsIndexRoot',
+ apolloProvider,
+ provide: {
+ newOrganizationUrl,
+ organizationsEmptyStateSvgPath,
+ },
+ render(createElement) {
+ return createElement(OrganizationsIndexApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index 17ab7bd1d34..d281a0d8a1c 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -4,10 +4,34 @@
// https://gitlab.com/gitlab-org/gitlab/-/issues/420777
// https://gitlab.com/gitlab-org/gitlab/-/issues/421441
-export const organization = {
- id: 'gid://gitlab/Organization/1',
- __typename: 'Organization',
-};
+export const organizations = [
+ {
+ id: 'gid://gitlab/Organization/1',
+ name: 'My First Organization',
+ descriptionHtml:
+ '<p>This is where an organization can be explained in <strong>detail</strong></p>',
+ avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61',
+ webUrl: '/-/organizations/default',
+ __typename: 'Organization',
+ },
+ {
+ id: 'gid://gitlab/Organization/2',
+ name: 'Vegetation Co.',
+ descriptionHtml:
+ '<p> Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt<script>alert(1)</script></p>',
+ avatarUrl: null,
+ webUrl: '/-/organizations/default',
+ __typename: 'Organization',
+ },
+ {
+ id: 'gid://gitlab/Organization/3',
+ name: 'Dude where is my car?',
+ descriptionHtml: null,
+ avatarUrl: null,
+ webUrl: '/-/organizations/default',
+ __typename: 'Organization',
+ },
+];
export const organizationProjects = {
nodes: [
@@ -256,3 +280,11 @@ export const organizationGroups = {
},
],
};
+
+export const createOrganizationResponse = {
+ organization: {
+ name: 'Default',
+ path: '/-/organizations/default',
+ },
+ errors: [],
+};
diff --git a/app/assets/javascripts/organizations/new/components/app.vue b/app/assets/javascripts/organizations/new/components/app.vue
new file mode 100644
index 00000000000..8f71fdfe68b
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/components/app.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
+import { createAlert } from '~/alert';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import createOrganizationMutation from '../graphql/mutations/create_organization.mutation.graphql';
+import NewEditForm from '../../shared/components/new_edit_form.vue';
+
+export default {
+ name: 'OrganizationNewApp',
+ components: { NewEditForm, GlSprintf, GlLink },
+ i18n: {
+ pageTitle: s__('Organization|New organization'),
+ pageDescription: s__(
+ 'Organization|%{linkStart}Organizations%{linkEnd} are a top-level container to hold your groups and projects.',
+ ),
+ errorMessage: s__('Organization|An error occurred creating an organization. Please try again.'),
+ successAlertTitle: s__('Organization|Organization successfully created.'),
+ successAlertMessage: s__('Organization|You can now start using your new organization.'),
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {
+ organizationsHelpPagePath() {
+ return helpPagePath('user/organization/index');
+ },
+ },
+ methods: {
+ async onSubmit(formValues) {
+ this.loading = true;
+ try {
+ const {
+ data: {
+ createOrganization: { organization, errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: createOrganizationMutation,
+ variables: {
+ ...formValues,
+ },
+ });
+
+ if (errors.length) {
+ // TODO: handle errors when using real API after https://gitlab.com/gitlab-org/gitlab/-/issues/417891 is complete.
+ return;
+ }
+
+ visitUrlWithAlerts(organization.path, [
+ {
+ id: 'organization-successfully-created',
+ title: this.$options.i18n.successAlertTitle,
+ message: this.$options.i18n.successAlertMessage,
+ variant: 'success',
+ },
+ ]);
+ } catch (error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-6">
+ <h1 class="gl-mt-0 gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1>
+ <p>
+ <gl-sprintf :message="$options.i18n.pageDescription">
+ <template #link="{ content }"
+ ><gl-link :href="organizationsHelpPagePath">{{ content }}</gl-link></template
+ >
+ </gl-sprintf>
+ </p>
+ <new-edit-form :loading="loading" @submit="onSubmit" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql b/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
new file mode 100644
index 00000000000..766c7e96d14
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
@@ -0,0 +1,9 @@
+mutation createOrganization($input: LocalCreateOrganizationInput!) {
+ createOrganization(input: $input) @client {
+ organization {
+ name
+ path
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/organizations/new/graphql/typedefs.graphql b/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
new file mode 100644
index 00000000000..f708c4ad162
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
@@ -0,0 +1,5 @@
+# TODO: Use real input type when https://gitlab.com/gitlab-org/gitlab/-/issues/417891 is complete.
+input LocalCreateOrganizationInput {
+ name: String
+ path: String
+}
diff --git a/app/assets/javascripts/organizations/new/index.js b/app/assets/javascripts/organizations/new/index.js
new file mode 100644
index 00000000000..a65603227f6
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/index.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import createDefaultClient from '~/lib/graphql';
+import resolvers from '../shared/graphql/resolvers';
+import App from './components/app.vue';
+
+export const initOrganizationsNew = () => {
+ const el = document.getElementById('js-organizations-new');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const { organizationsPath, rootUrl } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'OrganizationNewRoot',
+ apolloProvider,
+ provide: {
+ organizationsPath,
+ rootUrl,
+ },
+ render(createElement) {
+ return createElement(App);
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
new file mode 100644
index 00000000000..db33f240966
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
@@ -0,0 +1,125 @@
+<script>
+import {
+ GlForm,
+ GlFormFields,
+ GlButton,
+ GlFormInputGroup,
+ GlFormInput,
+ GlInputGroupText,
+ GlTruncate,
+} from '@gitlab/ui';
+import { formValidators } from '@gitlab/ui/dist/utils';
+import { s__, __ } from '~/locale';
+import { slugify } from '~/lib/utils/text_utility';
+import { joinPaths } from '~/lib/utils/url_utility';
+
+export default {
+ name: 'NewEditForm',
+ components: {
+ GlForm,
+ GlFormFields,
+ GlButton,
+ GlFormInputGroup,
+ GlFormInput,
+ GlInputGroupText,
+ GlTruncate,
+ },
+ i18n: {
+ createOrganization: s__('Organization|Create organization'),
+ cancel: __('Cancel'),
+ pathPlaceholder: s__('Organization|my-organization'),
+ },
+ formId: 'new-organization-form',
+ fields: {
+ name: {
+ label: s__('Organization|Organization name'),
+ validators: [formValidators.required(s__('Organization|Organization name is required.'))],
+ groupAttrs: {
+ description: s__(
+ 'Organization|Must start with a letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.',
+ ),
+ },
+ inputAttrs: {
+ class: 'gl-md-form-input-lg',
+ placeholder: s__('Organization|My organization'),
+ },
+ },
+ path: {
+ label: s__('Organization|Organization URL'),
+ validators: [formValidators.required(s__('Organization|Organization URL is required.'))],
+ },
+ },
+ inject: ['organizationsPath', 'rootUrl'],
+ props: {
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ formValues: {
+ name: '',
+ path: '',
+ },
+ hasPathBeenManuallySet: false,
+ };
+ },
+ computed: {
+ baseUrl() {
+ return joinPaths(this.rootUrl, this.organizationsPath, '/');
+ },
+ },
+ watch: {
+ 'formValues.name': function watchName(value) {
+ if (this.hasPathBeenManuallySet) {
+ return;
+ }
+
+ this.formValues.path = slugify(value);
+ },
+ },
+ methods: {
+ onPathInput(event, formFieldsInputEvent) {
+ formFieldsInputEvent(event);
+ this.hasPathBeenManuallySet = true;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form :id="$options.formId">
+ <gl-form-fields
+ v-model="formValues"
+ :form-id="$options.formId"
+ :fields="$options.fields"
+ @submit="$emit('submit', formValues)"
+ >
+ <template #input(path)="{ id, value, validation, input, blur }">
+ <gl-form-input-group>
+ <template #prepend>
+ <gl-input-group-text class="organization-root-path">
+ <gl-truncate :text="baseUrl" position="middle" />
+ </gl-input-group-text>
+ </template>
+ <gl-form-input
+ v-bind="validation"
+ :id="id"
+ :value="value"
+ :placeholder="$options.i18n.pathPlaceholder"
+ class="gl-h-auto! gl-md-form-input-lg"
+ @input="onPathInput($event, input)"
+ @blur="blur"
+ />
+ </gl-form-input-group>
+ </template>
+ </gl-form-fields>
+ <div class="gl-display-flex gl-gap-3">
+ <gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="loading">{{
+ $options.i18n.createOrganization
+ }}</gl-button>
+ <gl-button :href="organizationsPath">{{ $options.i18n.cancel }}</gl-button>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/organizations/shared/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index c78266b0476..9f7e9b22e1d 100644
--- a/app/assets/javascripts/organizations/shared/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -1,18 +1,44 @@
-import { organization, organizationProjects, organizationGroups } from '../../mock_data';
+import {
+ organizations,
+ organizationProjects,
+ organizationGroups,
+ createOrganizationResponse,
+} from '../../mock_data';
+
+const simulateLoading = () => {
+ return new Promise((resolve) => {
+ setTimeout(resolve, 1000);
+ });
+};
export default {
Query: {
organization: async () => {
// Simulate API loading
- await new Promise((resolve) => {
- setTimeout(resolve, 1000);
- });
+ await simulateLoading();
return {
- ...organization,
+ ...organizations[0],
projects: organizationProjects,
groups: organizationGroups,
};
},
},
+ UserCore: {
+ organizations: async () => {
+ await simulateLoading();
+
+ return {
+ nodes: organizations,
+ };
+ },
+ },
+ Mutation: {
+ createOrganization: async () => {
+ // Simulate API loading
+ await simulateLoading();
+
+ return createOrganizationResponse;
+ },
+ },
};
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index b58e2249829..7c594a6c091 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
@@ -231,6 +231,7 @@ export default {
v-if="hasNoTags"
:title="emptyStateTitle"
:svg-path="config.noContainersImage"
+ :svg-height="null"
:description="emptyStateDescription"
class="gl-mx-auto gl-my-0"
/>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue
index a68c4de5aa6..93bdb942faa 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state.vue
@@ -15,6 +15,7 @@ export default {
<gl-empty-state
:title="s__('ContainerRegistry|There are no container images available in this group')"
:svg-path="config.noContainersImage"
+ :svg-height="null"
>
<template #description>
<p>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue
index 5aa04419ca0..4ddcaa5c9a7 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state.vue
@@ -41,6 +41,7 @@ export default {
<gl-empty-state
:title="s__('ContainerRegistry|There are no container images stored for this project')"
:svg-path="config.noContainersImage"
+ :svg-height="null"
>
<template #description>
<p>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index c266dbf7e98..3eb1b2b4ba5 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
@@ -190,6 +190,7 @@ export default {
:title="$options.i18n.MISSING_OR_DELETED_IMAGE_TITLE"
:description="$options.i18n.MISSING_OR_DELETED_IMAGE_MESSAGE"
:svg-path="config.noContainersImage"
+ :svg-height="null"
class="gl-mx-auto gl-my-0"
/>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index df87ee79111..a1c4d7ea1f2 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
@@ -251,6 +251,7 @@ export default {
v-if="showConnectionError"
:title="$options.i18n.CONNECTION_ERROR_TITLE"
:svg-path="config.containersErrorImage"
+ :svg-height="null"
>
<template #description>
<p>
@@ -325,6 +326,7 @@ export default {
<gl-empty-state
v-else
:svg-path="config.noContainersImage"
+ :svg-height="null"
data-testid="emptySearch"
:title="$options.i18n.EMPTY_RESULT_TITLE"
>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue
index b0d03a7cebe..7a29cb2d5ab 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue
@@ -35,7 +35,11 @@ export default {
</script>
<template>
- <gl-empty-state :svg-path="noManifestsIllustration" :title="$options.i18n.noManifestTitle">
+ <gl-empty-state
+ :svg-path="noManifestsIllustration"
+ :svg-height="null"
+ :title="$options.i18n.noManifestTitle"
+ >
<template #description>
<p class="gl-mb-5">
<gl-sprintf :message="$options.i18n.emptyText">
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue
index b55204de875..65ca4de7055 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue
@@ -73,6 +73,7 @@ export default {
v-if="hasNoTags"
:title="emptyStateTitle"
:svg-path="noContainersImage"
+ :svg-height="null"
:description="emptyStateDescription"
class="gl-mx-auto gl-my-0"
/>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue
index b34d3a950c0..ea265430865 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue
@@ -62,6 +62,7 @@ export default {
v-else-if="hasNoTags"
:title="emptyStateTitle"
:svg-path="noContainersImage"
+ :svg-height="null"
:description="emptyStateDescription"
class="gl-mx-auto gl-my-0"
/>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
index 1d8cb0f1360..9daed3e1211 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
@@ -164,6 +164,7 @@ export default {
v-if="showConnectionError"
:title="$options.i18n.connectionErrorTitle"
:svg-path="containersErrorImage"
+ :svg-height="null"
>
<template #description>
<p>
@@ -220,6 +221,7 @@ export default {
<gl-empty-state
v-else
:svg-path="noContainersImage"
+ :svg-height="null"
data-testid="emptySearch"
:title="emptyStateTexts.title"
>
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
index cb96f3d96cb..b49c448c478 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
@@ -146,6 +146,7 @@ export default {
:title="s__('PackageRegistry|Unable to load package')"
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
:svg-path="svgPath"
+ :svg-height="null"
/>
<div v-else class="packages-app">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index d1982464eb9..265e3de0512 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -245,6 +245,7 @@ export default {
:title="s__('PackageRegistry|Unable to load package')"
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
:svg-path="emptyListIllustration"
+ :svg-height="null"
/>
<div v-else-if="projectName" class="packages-app">
<package-title :package-entity="packageEntity">
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue
index 9ac1673dbf3..5a7feba35a4 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue
@@ -70,7 +70,7 @@ export default {
<gl-form-input
:id="id"
:disabled="duplicatesAllowed || loading"
- size="lg"
+ width="lg"
:value="duplicateExceptionRegex"
:state="isExceptionRegexValid"
@change="update(name, $event)"
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
index cde46c3da50..cd6c9677b5f 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
@@ -268,7 +268,7 @@ export default {
name="application_setting[signup_enabled]"
:help-text="signupEnabledHelpText"
:label="$options.i18n.signupEnabledLabel"
- data-qa-selector="signup_enabled_checkbox"
+ data-testid="signup-enabled-checkbox"
/>
<signup-checkbox
@@ -277,7 +277,6 @@ export default {
name="application_setting[require_admin_approval_after_user_signup]"
:help-text="requireAdminApprovalHelpText"
:label="$options.i18n.requireAdminApprovalLabel"
- data-qa-selector="require_admin_approval_after_user_signup_checkbox"
data-testid="require-admin-approval-checkbox"
/>
@@ -452,7 +451,7 @@ export default {
</section>
<gl-button
- data-qa-selector="save_changes_button"
+ data-testid="save-changes-button"
variant="confirm"
@click.prevent="submitButtonHandler"
>
diff --git a/app/assets/javascripts/pages/groups/custom_emoji/index.js b/app/assets/javascripts/pages/groups/custom_emoji/index.js
new file mode 100644
index 00000000000..dd02a6f5348
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/custom_emoji/index.js
@@ -0,0 +1,3 @@
+import { initCustomEmojis } from '~/custom_emoji/custom_emoji_bundle';
+
+requestIdleCallback(initCustomEmojis);
diff --git a/app/assets/javascripts/pages/groups/observability/dashboards/index.js b/app/assets/javascripts/pages/groups/observability/dashboards/index.js
deleted file mode 100644
index c3b6ce6f99f..00000000000
--- a/app/assets/javascripts/pages/groups/observability/dashboards/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import ObservabilityApp from '~/observability';
-
-ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/observability/datasources/index.js b/app/assets/javascripts/pages/groups/observability/datasources/index.js
deleted file mode 100644
index c3b6ce6f99f..00000000000
--- a/app/assets/javascripts/pages/groups/observability/datasources/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import ObservabilityApp from '~/observability';
-
-ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/observability/explore/index.js b/app/assets/javascripts/pages/groups/observability/explore/index.js
deleted file mode 100644
index c3b6ce6f99f..00000000000
--- a/app/assets/javascripts/pages/groups/observability/explore/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import ObservabilityApp from '~/observability';
-
-ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/observability/manage/index.js b/app/assets/javascripts/pages/groups/observability/manage/index.js
deleted file mode 100644
index c3b6ce6f99f..00000000000
--- a/app/assets/javascripts/pages/groups/observability/manage/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import ObservabilityApp from '~/observability';
-
-ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/work_items/show/index.js b/app/assets/javascripts/pages/groups/work_items/show/index.js
new file mode 100644
index 00000000000..c091fbcc2b2
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/work_items/show/index.js
@@ -0,0 +1,4 @@
+import { WORKSPACE_GROUP } from '~/issues/constants';
+import { initWorkItemsRoot } from '~/work_items';
+
+initWorkItemsRoot(WORKSPACE_GROUP);
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index 1d0eaae4c57..459546a5562 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -168,13 +168,22 @@ export default {
}
},
- getFullDestinationUrl(params) {
+ destinationLinkHref(params) {
return joinPaths(gon.relative_url_root || '', '/', params.destination_full_path);
},
- getPresentationUrl(item) {
+ pathWithSuffix(path, item) {
const suffix = item.entity_type === WORKSPACE_GROUP ? '/' : '';
- return `${item.destination_full_path}${suffix}`;
+ return `${path}${suffix}`;
+ },
+
+ destinationLinkText(item) {
+ return this.pathWithSuffix(item.destination_full_path, item);
+ },
+
+ destinationText(item) {
+ const fullPath = joinPaths(item.destination_namespace, item.destination_slug);
+ return this.pathWithSuffix(fullPath, item);
},
getEntityTooltip(item) {
@@ -187,6 +196,11 @@ export default {
return '';
}
},
+
+ setPageSize(size) {
+ this.paginationConfig.perPage = size;
+ this.paginationConfig.page = 1;
+ },
},
gitlabLogo: window.gon.gitlab_logo,
@@ -218,19 +232,21 @@ export default {
class="gl-w-full"
>
<template #cell(destination_name)="{ item }">
- <template v-if="item.destination_full_path">
- <gl-icon
- v-gl-tooltip
- :name="item.entity_type"
- :title="getEntityTooltip(item)"
- :aria-label="getEntityTooltip(item)"
- class="gl-text-gray-500"
- />
- <gl-link :href="getFullDestinationUrl(item)" target="_blank">
- {{ getPresentationUrl(item) }}
- </gl-link>
- </template>
- <gl-loading-icon v-else inline />
+ <gl-icon
+ v-gl-tooltip
+ :name="item.entity_type"
+ :title="getEntityTooltip(item)"
+ :aria-label="getEntityTooltip(item)"
+ class="gl-text-gray-500"
+ />
+ <gl-link
+ v-if="item.destination_full_path"
+ :href="destinationLinkHref(item)"
+ target="_blank"
+ >
+ {{ destinationLinkText(item) }}
+ </gl-link>
+ <span v-else>{{ destinationText(item) }}</span>
</template>
<template #cell(created_at)="{ value }">
<time-ago :time="value" />
@@ -253,7 +269,7 @@ export default {
:page-info="pageInfo"
class="gl-m-0 gl-mt-3"
@set-page="paginationConfig.page = $event"
- @set-page-size="paginationConfig.perPage = $event"
+ @set-page-size="setPageSize"
/>
</template>
<local-storage-sync
diff --git a/app/assets/javascripts/pages/organizations/organizations/index/index.js b/app/assets/javascripts/pages/organizations/organizations/index/index.js
new file mode 100644
index 00000000000..c7e087b81c6
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/index/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsIndex } from '~/organizations/index';
+
+initOrganizationsIndex();
diff --git a/app/assets/javascripts/pages/organizations/organizations/new/index.js b/app/assets/javascripts/pages/organizations/organizations/new/index.js
new file mode 100644
index 00000000000..ab23fbf155d
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/new/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsNew } from '~/organizations/new';
+
+initOrganizationsNew();
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index a3d930433c3..07662e4411e 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -21,6 +21,7 @@ import RefSelector from '~/ref/components/ref_selector.vue';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
import HighlightWorker from '~/vue_shared/components/source_viewer/workers/highlight_worker?worker';
+import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
Vue.use(Vuex);
Vue.use(VueApollo);
@@ -62,6 +63,7 @@ const initRefSwitcher = () => {
};
initRefSwitcher();
+initAmbiguousRefModal();
if (viewBlobEl) {
const {
diff --git a/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js b/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js
index 9a3bb25de70..ffd4ef9efbb 100644
--- a/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js
+++ b/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { s__ } from '~/locale';
import Translate from '~/vue_shared/translate';
import RefSelector from '~/ref/components/ref_selector.vue';
-import { visitUrl } from '~/lib/utils/url_utility';
+import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { generateRefDestinationPath } from './ref_switcher_utils';
Vue.use(Translate);
@@ -13,7 +13,7 @@ export default () => {
const el = document.getElementById('js-blob-ref-switcher');
if (!el) return false;
- const { projectId, ref, namespace } = el.dataset;
+ const { projectId, ref, refType, namespace } = el.dataset;
return new Vue({
el,
@@ -21,7 +21,8 @@ export default () => {
return createElement(RefSelector, {
props: {
projectId,
- value: ref,
+ value: refType ? joinPaths('refs', refType, ref) : ref,
+ useSymbolicRefNames: Boolean(refType),
translations: {
dropdownHeader: REF_SWITCH_HEADER,
searchPlaceholder: REF_SWITCH_HEADER,
diff --git a/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js b/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js
index 5fecd024f1a..21a30f1c54b 100644
--- a/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js
+++ b/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js
@@ -10,19 +10,32 @@ export function generateRefDestinationPath(selectedRef, namespace) {
return window.location.href;
}
+ let refType = null;
const { pathname } = window.location;
const encodedHash = '%23';
const [projectRootPath] = pathname.split(namespace);
+ let actualRef = selectedRef;
+
+ const matches = selectedRef.match(/^refs\/(heads|tags)\/(.+)/);
+ if (matches) {
+ [, refType, actualRef] = matches;
+ }
const destinationPath = joinPaths(
projectRootPath,
namespace,
- encodeURI(selectedRef).replace(/#/g, encodedHash),
+ encodeURI(actualRef).replace(/#/g, encodedHash),
);
const newURL = new URL(window.location);
newURL.pathname = destinationPath;
+ if (refType) {
+ newURL.searchParams.set('ref_type', refType.toLowerCase());
+ } else {
+ newURL.searchParams.delete('ref_type');
+ }
+
return newURL.href;
}
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 e207df2434b..22c21430e8b 100644
--- a/app/assets/javascripts/pages/projects/find_file/show/index.js
+++ b/app/assets/javascripts/pages/projects/find_file/show/index.js
@@ -6,8 +6,9 @@ import InitBlobRefSwitcher from '../ref_switcher';
InitBlobRefSwitcher();
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,
+ refType: findElement.dataset.refType,
});
+projectFindFile.load(findElement.dataset.fileFindUrl);
new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 10c794c9ba2..c24a69bc26b 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -1,6 +1,5 @@
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Vue from 'vue';
-import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
import { __ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
@@ -10,205 +9,203 @@ import SeriesDataMixin from './series_data_mixin';
const seriesDataToBarData = (raw) => Object.entries(raw).map(([name, data]) => ({ name, data }));
-waitForCSSLoaded(() => {
- const languagesContainer = document.getElementById('js-languages-chart');
- const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
- const monthContainer = document.getElementById('js-month-chart');
- const weekdayContainer = document.getElementById('js-weekday-chart');
- const hourContainer = document.getElementById('js-hour-chart');
- const branchSelector = document.getElementById('js-project-graph-ref-switcher');
- const LANGUAGE_CHART_HEIGHT = 300;
- const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
- if (firstDayOfWeek === 0) {
- return weekDays;
- }
+const languagesContainer = document.getElementById('js-languages-chart');
+const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
+const monthContainer = document.getElementById('js-month-chart');
+const weekdayContainer = document.getElementById('js-weekday-chart');
+const hourContainer = document.getElementById('js-hour-chart');
+const branchSelector = document.getElementById('js-project-graph-ref-switcher');
+const LANGUAGE_CHART_HEIGHT = 300;
+const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
+ if (firstDayOfWeek === 0) {
+ return weekDays;
+ }
- return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
- const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
+ return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
+ const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
- return {
- ...acc,
- [reorderedDayName]: weekDays[reorderedDayName],
- };
- }, {});
- };
+ return {
+ ...acc,
+ [reorderedDayName]: weekDays[reorderedDayName],
+ };
+ }, {});
+};
- // eslint-disable-next-line no-new
- new Vue({
- el: languagesContainer,
- components: {
- GlColumnChart,
- },
- data() {
- return {
- chartData: JSON.parse(languagesContainer.dataset.chartData),
- };
- },
- computed: {
- seriesData() {
- return [{ name: 'full', data: this.chartData.map((d) => [d.label, d.value]) }];
+// eslint-disable-next-line no-new
+new Vue({
+ el: languagesContainer,
+ components: {
+ GlColumnChart,
+ },
+ data() {
+ return {
+ chartData: JSON.parse(languagesContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ return [{ name: 'full', data: this.chartData.map((d) => [d.label, d.value]) }];
+ },
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: this.seriesData,
+ xAxisTitle: __('Used programming language'),
+ yAxisTitle: __('Percentage'),
+ xAxisType: 'category',
},
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: this.seriesData,
- xAxisTitle: __('Used programming language'),
- yAxisTitle: __('Percentage'),
- xAxisType: 'category',
- },
- attrs: {
- height: LANGUAGE_CHART_HEIGHT,
- responsive: true,
- },
- });
- },
- });
+ attrs: {
+ height: LANGUAGE_CHART_HEIGHT,
+ responsive: true,
+ },
+ });
+ },
+});
- const {
- graphEndpoint,
- graphEndDate,
- graphStartDate,
- graphRef,
- graphCsvPath,
- } = codeCoverageContainer.dataset;
- // eslint-disable-next-line no-new
- new Vue({
- el: codeCoverageContainer,
- render(h) {
- return h(CodeCoverage, {
- props: {
- graphEndpoint,
- graphEndDate,
- graphStartDate,
- graphRef,
- graphCsvPath,
- },
- });
- },
- });
+const {
+ graphEndpoint,
+ graphEndDate,
+ graphStartDate,
+ graphRef,
+ graphCsvPath,
+} = codeCoverageContainer.dataset;
+// eslint-disable-next-line no-new
+new Vue({
+ el: codeCoverageContainer,
+ render(h) {
+ return h(CodeCoverage, {
+ props: {
+ graphEndpoint,
+ graphEndDate,
+ graphStartDate,
+ graphRef,
+ graphCsvPath,
+ },
+ });
+ },
+});
- // eslint-disable-next-line no-new
- new Vue({
- el: monthContainer,
- components: {
- GlColumnChart,
- },
- mixins: [SeriesDataMixin],
- data() {
- return {
- chartData: JSON.parse(monthContainer.dataset.chartData),
- };
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: seriesDataToBarData(this.seriesData),
- xAxisTitle: __('Day of month'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- attrs: {
- responsive: true,
- },
- });
- },
- });
+// eslint-disable-next-line no-new
+new Vue({
+ el: monthContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(monthContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: seriesDataToBarData(this.seriesData),
+ xAxisTitle: __('Day of month'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ attrs: {
+ responsive: true,
+ },
+ });
+ },
+});
- // eslint-disable-next-line no-new
- new Vue({
- el: weekdayContainer,
- components: {
- GlColumnChart,
- },
- data() {
- return {
- chartData: JSON.parse(weekdayContainer.dataset.chartData),
- };
- },
- computed: {
- seriesData() {
- const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
- const data = Object.keys(weekDays).reduce((acc, key) => {
- acc.push([key, weekDays[key]]);
- return acc;
- }, []);
- return [{ name: 'full', data }];
+// eslint-disable-next-line no-new
+new Vue({
+ el: weekdayContainer,
+ components: {
+ GlColumnChart,
+ },
+ data() {
+ return {
+ chartData: JSON.parse(weekdayContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
+ const data = Object.keys(weekDays).reduce((acc, key) => {
+ acc.push([key, weekDays[key]]);
+ return acc;
+ }, []);
+ return [{ name: 'full', data }];
+ },
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: this.seriesData,
+ xAxisTitle: __('Weekday'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
},
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: this.seriesData,
- xAxisTitle: __('Weekday'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- attrs: {
- responsive: true,
- },
- });
- },
- });
+ attrs: {
+ responsive: true,
+ },
+ });
+ },
+});
- // eslint-disable-next-line no-new
- new Vue({
- el: hourContainer,
- components: {
- GlColumnChart,
- },
- mixins: [SeriesDataMixin],
- data() {
- return {
- chartData: JSON.parse(hourContainer.dataset.chartData),
- };
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: seriesDataToBarData(this.seriesData),
- xAxisTitle: __('Hour (UTC)'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- attrs: {
- responsive: true,
- },
- });
- },
- });
+// eslint-disable-next-line no-new
+new Vue({
+ el: hourContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(hourContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: seriesDataToBarData(this.seriesData),
+ xAxisTitle: __('Hour (UTC)'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ attrs: {
+ responsive: true,
+ },
+ });
+ },
+});
- const { projectId, projectBranch, graphPath } = branchSelector.dataset;
+const { projectId, projectBranch, graphPath } = branchSelector.dataset;
- const GRAPHS_PATH_REGEX = /^(.*?)\/-\/graphs/g;
- const graphsPathPrefix = graphPath.match(GRAPHS_PATH_REGEX)?.[0];
- if (!graphsPathPrefix) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('Path is not correct');
- }
+const GRAPHS_PATH_REGEX = /^(.*?)\/-\/graphs/g;
+const graphsPathPrefix = graphPath.match(GRAPHS_PATH_REGEX)?.[0];
+if (!graphsPathPrefix) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Path is not correct');
+}
- // eslint-disable-next-line no-new
- new Vue({
- el: branchSelector,
- name: 'RefSelector',
- render(createComponent) {
- return createComponent(RefSelector, {
- props: {
- enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
- value: projectBranch,
- translations: {
- dropdownHeader: __('Switch branch/tag'),
- searchPlaceholder: __('Search branches and tags'),
- },
- projectId,
+// eslint-disable-next-line no-new
+new Vue({
+ el: branchSelector,
+ name: 'RefSelector',
+ render(createComponent) {
+ return createComponent(RefSelector, {
+ props: {
+ enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
+ value: projectBranch,
+ translations: {
+ dropdownHeader: __('Switch branch/tag'),
+ searchPlaceholder: __('Search branches and tags'),
},
- class: 'gl-w-20',
- on: {
- input(selected) {
- visitUrl(`${graphsPathPrefix}/${encodeURIComponent(selected)}/charts`);
- },
+ projectId,
+ },
+ class: 'gl-w-20',
+ on: {
+ input(selected) {
+ visitUrl(`${graphsPathPrefix}/${encodeURIComponent(selected)}/charts`);
},
- });
- },
- });
+ },
+ });
+ },
});
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 ead15143072..4118541d973 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,6 +1,4 @@
-import { initFilteredSearchServiceDesk } from '~/issues';
-import { mountServiceDeskListApp } from '~/issues/service_desk';
+import { initFilteredSearchServiceDesk, mountServiceDeskListApp } from '~/issues/service_desk';
initFilteredSearchServiceDesk();
-
mountServiceDeskListApp();
diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js
index cd83f2b7b64..6618b68e9fe 100644
--- a/app/assets/javascripts/pages/projects/jobs/show/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/show/index.js
@@ -1,3 +1,3 @@
-import initJobDetails from '~/ci/job_details';
+import { initJobDetails } from '~/ci/job_details';
initJobDetails();
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 d23a0615bb8..8cb1462c883 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
@@ -43,6 +43,7 @@ if (mrNewCompareNode) {
project: 'js-source-project',
branch: 'js-source-branch gl-font-monospace',
},
+ compareSide: 'source',
},
methods: {
async selectedBranch(branchName) {
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 2cdbf0fb830..af1635221ab 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
@@ -4,8 +4,9 @@ import { s__ } from '~/locale';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { initPipelineCountListener } from '~/commit/pipelines/utils';
import { initIssuableSidebar } from '~/issuable';
-import MergeRequestStatusBadge from '~/merge_requests/components/merge_request_status_badge.vue';
+import MergeRequestHeader from '~/merge_requests/components/merge_request_header.vue';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
@@ -14,7 +15,7 @@ import { initMrExperienceSurvey } from '~/surveys/merge_request_experience';
import toast from '~/vue_shared/plugins/global_toast';
import getStateQuery from './queries/get_state.query.graphql';
-export default function initMergeRequestShow() {
+export default function initMergeRequestShow(store) {
new ZenMode(); // eslint-disable-line no-new
initPipelineCountListener(document.querySelector('#commit-pipeline-table-view'));
new ShortcutsIssuable(true); // eslint-disable-line no-new
@@ -23,26 +24,27 @@ export default function initMergeRequestShow() {
initAwardsApp(document.getElementById('js-vue-awards-block'));
initMrExperienceSurvey();
- const el = document.querySelector('.js-mr-status-box');
- const { iid, issuableType, projectPath, state } = el.dataset;
+ const el = document.querySelector('.js-mr-header');
+ const { hidden, iid, projectPath, state } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
- name: 'IssuableStatusBoxRoot',
+ name: 'MergeRequestHeaderRoot',
+ store,
apolloProvider: new VueApollo({
defaultClient: createDefaultClient(),
}),
provide: {
query: getStateQuery,
+ hidden: parseBoolean(hidden),
iid,
projectPath,
},
render(createElement) {
- return createElement(MergeRequestStatusBadge, {
+ return createElement(MergeRequestHeader, {
props: {
initialState: state,
- issuableType,
},
});
},
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
index f7b522f7c85..fb243d01dc6 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/page.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import initMrNotes from 'ee_else_ce/mr_notes';
-import { mountHeaderMetadata } from '~/merge_requests';
import StickyHeader from '~/merge_requests/components/sticky_header.vue';
import { start as startCodeReviewMessaging } from '~/code_review/signals';
import diffsEventHub from '~/diffs/event_hub';
@@ -17,14 +16,13 @@ Vue.use(VueApollo);
export function initMrPage() {
initMrNotes();
- initShow();
+ initShow(store);
initMrMoreDropdown();
startCodeReviewMessaging({ signalBus: diffsEventHub });
}
requestIdleCallback(() => {
initSidebarBundle(store);
- mountHeaderMetadata(store);
const el = document.getElementById('js-merge-sticky-header');
diff --git a/app/assets/javascripts/pages/projects/ml/models/show/index.js b/app/assets/javascripts/pages/projects/ml/models/show/index.js
new file mode 100644
index 00000000000..87ee5c851f6
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/ml/models/show/index.js
@@ -0,0 +1,4 @@
+import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+import { ShowMlModel } from '~/ml/model_registry/apps';
+
+initSimpleApp('#js-mount-show-ml-model', ShowMlModel);
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 642fd56eab1..9c4582ece21 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,12 +1,5 @@
<script>
-import {
- GlFormRadio,
- GlFormRadioGroup,
- GlIcon,
- GlLink,
- GlSprintf,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlFormRadio, GlFormRadioGroup, GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { getWeekdayNames } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -23,7 +16,6 @@ export default {
GlFormRadioGroup,
GlIcon,
GlLink,
- GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -97,8 +89,7 @@ export default {
},
{
value: KEY_CUSTOM,
- text: s__('PipelineScheduleIntervalPattern|Custom (%{linkStart}Learn more%{linkEnd}.)'),
- link: this.cronSyntaxUrl,
+ text: s__('PipelineScheduleIntervalPattern|Custom'),
},
];
},
@@ -155,6 +146,10 @@ export default {
return value === KEY_CUSTOM && this.dailyLimit;
},
},
+ i18n: {
+ learnCronSyntax: s__('PipelineScheduleIntervalPattern|Set a custom interval with Cron syntax.'),
+ cronSyntaxLink: s__('PipelineScheduleIntervalPattern|What is Cron syntax?'),
+ },
};
</script>
@@ -167,19 +162,14 @@ export default {
:value="option.value"
:data-testid="option.value"
>
- <gl-sprintf v-if="option.link" :message="option.text">
- <template #link="{ content }">
- <gl-link :href="option.link" target="_blank" class="gl-font-sm">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
-
- <template v-else>{{ option.text }}</template>
+ {{ option.text }}
<gl-icon
v-if="showDailyLimitMessage(option)"
v-gl-tooltip.hover
name="question-o"
:title="scheduleDailyLimitMsg"
+ data-testid="daily-limit"
/>
</gl-form-radio>
</gl-form-radio-group>
@@ -193,5 +183,11 @@ export default {
required="true"
@input="onCustomInput"
/>
+ <p class="gl-mt-1 gl-mb-0 gl-text-secondary">
+ {{ $options.i18n.learnCronSyntax }}
+ <gl-link :href="cronSyntaxUrl" target="_blank">
+ {{ $options.i18n.cronSyntaxLink }}
+ </gl-link>
+ </p>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index bee0731d711..98c58515d24 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -7,6 +7,7 @@ import initTerraformNotification from '~/projects/terraform_notification';
import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
import initForksButton from '~/forks/init_forks_button';
+import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
// Project show page loads different overview content based on user preferences
if (document.getElementById('js-tree-list')) {
@@ -45,6 +46,7 @@ initTerraformNotification();
initReadMore();
initStarButton();
+initAmbiguousRefModal();
if (document.querySelector('.js-autodevops-banner')) {
import(/* webpackChunkName: 'userCallOut' */ '~/user_callout')
diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js
index 17c17014ece..d87f8898c63 100644
--- a/app/assets/javascripts/pages/projects/tree/show/index.js
+++ b/app/assets/javascripts/pages/projects/tree/show/index.js
@@ -2,7 +2,9 @@ import $ from 'jquery';
import initTree from 'ee_else_ce/repository';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import NewCommitForm from '~/new_commit_form';
+import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new
initTree();
+initAmbiguousRefModal();
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/work_items/index.js b/app/assets/javascripts/pages/projects/work_items/index.js
index 11c257611f0..b44ca708b28 100644
--- a/app/assets/javascripts/pages/projects/work_items/index.js
+++ b/app/assets/javascripts/pages/projects/work_items/index.js
@@ -1,3 +1,3 @@
-import { initWorkItemsRoot } from '~/work_items/index';
+import { initWorkItemsRoot } from '~/work_items';
initWorkItemsRoot();
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index 84050c3cb0f..90a9c9e7279 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -1,5 +1,3 @@
-import { trackNewRegistrations } from '~/google_tag_manager';
-
import NoEmojiValidator from '~/emoji/no_emoji_validator';
import LengthValidator from '~/validators/length_validator';
import UsernameValidator from '~/pages/sessions/new/username_validator';
@@ -13,8 +11,6 @@ new LengthValidator(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
new EmailFormatValidator(); // eslint-disable-line no-new
-trackNewRegistrations();
-
Tracking.enableFormTracking({
forms: { allow: ['new_user'] },
});
diff --git a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
index 3792dad376b..3c070d2708d 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
@@ -77,7 +77,7 @@ export default {
v-gl-modal="$options.modal.modalId"
category="secondary"
variant="danger"
- data-qa-selector="delete_button"
+ data-qa-selector="delete-button"
>
{{ $options.i18n.deletePageText }}
</gl-button>
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 553cb1f0464..eaa99556994 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -317,7 +317,7 @@ export default {
name="wiki[title]"
type="text"
class="form-control"
- data-qa-selector="wiki_title_textbox"
+ data-testid="wiki-title-textbox"
:required="true"
:autofocus="!pageInfo.persisted"
:placeholder="$options.i18n.title.placeholder"
@@ -397,7 +397,7 @@ export default {
name="wiki[message]"
type="text"
class="form-control"
- data-qa-selector="wiki_message_textbox"
+ data-testid="wiki-message-textbox"
:placeholder="$options.i18n.commitMessage.label"
/>
</gl-form-group>
@@ -409,7 +409,6 @@ export default {
category="primary"
variant="confirm"
type="submit"
- data-qa-selector="wiki_submit_button"
data-testid="wiki-submit-button"
:disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
diff --git a/app/assets/javascripts/pages/users/terms/index/index.js b/app/assets/javascripts/pages/users/terms/index/index.js
index 29ddde6da94..3619bcff65c 100644
--- a/app/assets/javascripts/pages/users/terms/index/index.js
+++ b/app/assets/javascripts/pages/users/terms/index/index.js
@@ -1,4 +1,3 @@
import { initTermsApp } from '~/terms';
-import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
-waitForCSSLoaded(initTermsApp);
+initTermsApp();
diff --git a/app/assets/javascripts/performance_bar/components/add_request.vue b/app/assets/javascripts/performance_bar/components/add_request.vue
index 6702c49030b..9a8ebedaf15 100644
--- a/app/assets/javascripts/performance_bar/components/add_request.vue
+++ b/app/assets/javascripts/performance_bar/components/add_request.vue
@@ -1,5 +1,5 @@
<script>
-import { GlForm, GlFormInput, GlButton } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
@@ -12,6 +12,9 @@ export default {
GlButton,
GlFormInput,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
data() {
return {
inputEnabled: false,
@@ -37,7 +40,8 @@ export default {
<div id="peek-view-add-request" class="view gl-display-flex">
<gl-form class="gl-display-flex gl-align-items-center" @submit.prevent>
<gl-button
- class="gl-text-blue-300! gl-mr-2"
+ v-gl-tooltip.viewport
+ class="gl-mr-2"
category="tertiary"
variant="link"
icon="plus"
@@ -52,7 +56,7 @@ export default {
type="text"
:placeholder="$options.i18n.inputLabel"
:aria-label="$options.i18n.inputLabel"
- class="gl-ml-2"
+ class="gl-ml-2 gl-px-3! gl-py-2!"
@keyup.enter="addRequest"
@keyup.esc="clearForm"
/>
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index b53e2709f83..ab10283b3c4 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,5 +1,11 @@
<script>
-import { GlButton, GlModal, GlModalDirective, GlCollapsibleListbox } from '@gitlab/ui';
+import {
+ GlButton,
+ GlTooltipDirective,
+ GlModal,
+ GlModalDirective,
+ GlCollapsibleListbox,
+} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { sortOrders, sortOrderOptions } from '../constants';
@@ -13,6 +19,7 @@ export default {
GlCollapsibleListbox,
},
directives: {
+ GlTooltip: GlTooltipDirective,
'gl-modal': GlModalDirective,
},
props: {
@@ -133,14 +140,17 @@ export default {
<div
v-if="currentRequest.details && metricDetails"
:id="`peek-view-${metric}`"
- class="gl-display-flex gl-align-items-center view"
+ class="gl-display-flex gl-align-items-baseline view"
data-qa-selector="detailed_metric_content"
>
- <gl-button v-gl-modal="modalId" class="gl-mr-2" type="button" variant="link">
- <span
- class="gl-text-blue-200 gl-font-weight-bold"
- data-testid="performance-bar-details-label"
- >
+ <gl-button
+ v-gl-tooltip.viewport
+ v-gl-modal="modalId"
+ class="gl-mr-2"
+ :title="header"
+ variant="link"
+ >
+ <span class="gl-font-sm gl-font-weight-semibold" data-testid="performance-bar-details-label">
{{ metricDetailsLabel }}
</span>
</gl-button>
@@ -150,7 +160,7 @@ export default {
<div v-for="(value, name) in metricDetailsSummary" :key="name" class="gl-pr-8">
<div v-if="value" data-testid="performance-bar-summary-item">
<div>{{ name }}</div>
- <div class="gl-font-size-h1 gl-font-weight-bold">{{ value }}</div>
+ <div class="gl-font-size-h1 gl-font-weight-semibold">{{ value }}</div>
</div>
</div>
</div>
@@ -178,7 +188,7 @@ export default {
v-for="(key, keyIndex) in keys"
:key="key"
class="text-break-word"
- :class="{ 'mb-3 bold': keyIndex == 0 }"
+ :class="{ 'mb-3 gl-font-weight-semibold': keyIndex == 0 }"
>
{{ item[key] }}
<gl-button
@@ -214,7 +224,7 @@ export default {
<div></div>
</template>
</gl-modal>
- {{ actualTitle }}
+ <span class="gl-opacity-7">{{ actualTitle }}</span>
<request-warning :html-id="htmlId" :warnings="warnings" />
</div>
</template>
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 128c744f282..720c1e0d7f2 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -1,7 +1,5 @@
<script>
-import { GlLink, GlPopover } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { glEmojiTag } from '~/emoji';
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
@@ -11,14 +9,13 @@ import RequestSelector from './request_selector.vue';
export default {
components: {
- GlPopover,
AddRequest,
DetailedMetric,
GlLink,
RequestSelector,
},
directives: {
- SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
props: {
store: {
@@ -123,11 +120,8 @@ export default {
hasHost() {
return this.currentRequest && this.currentRequest.details && this.currentRequest.details.host;
},
- birdEmoji() {
- if (this.hasHost && this.currentRequest.details.host.canary) {
- return glEmojiTag('baby_chick');
- }
- return '';
+ isCanary() {
+ return Boolean(this.currentRequest.details.host.canary);
},
downloadPath() {
const data = JSON.stringify(this.requests);
@@ -165,7 +159,6 @@ export default {
this.currentRequest = this.requestId;
},
methods: {
- glEmojiTag,
changeCurrentRequest(newRequestId) {
this.currentRequest = newRequestId;
this.$emit('change-request', newRequestId);
@@ -180,96 +173,117 @@ export default {
return this.store.findRequest(requestId)?.method?.toUpperCase() === 'GET';
},
},
- safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
<template>
<div id="js-peek" :class="env">
<div
v-if="currentRequest"
- class="d-flex container-fluid container-limited justify-content-center gl-align-items-center"
+ class="gl-display-flex container-fluid gl-overflow-x-auto"
data-qa-selector="performance_bar"
>
- <div id="peek-view-host" class="view">
- <span
- v-if="hasHost"
- class="current-host"
- :class="{ canary: currentRequest.details.host.canary }"
+ <div class="gl-display-flex gl-flex-shrink-0 view-performance-container">
+ <div v-if="hasHost" id="peek-view-host" class="gl-display-flex gl-gap-2 view">
+ <span class="current-host" :class="{ canary: isCanary }">
+ <gl-emoji
+ v-if="isCanary"
+ id="canary-emoji"
+ v-gl-tooltip.viewport="'Canary'"
+ data-name="baby_chick"
+ />
+ <gl-emoji
+ id="host-emoji"
+ v-gl-tooltip.viewport="currentRequest.details.host.hostname"
+ data-name="computer"
+ />
+ </span>
+ </div>
+ <detailed-metric
+ v-for="metric in $options.detailedMetrics"
+ :key="metric.metric"
+ :current-request="currentRequest"
+ :metric="metric.metric"
+ :title="metric.title"
+ :header="metric.header"
+ :keys="metric.keys"
+ />
+ <div
+ v-if="currentRequest.details && currentRequest.details.tracing"
+ id="peek-view-trace"
+ class="view"
>
- <span id="canary-emoji" v-safe-html:[$options.safeHtmlConfig]="birdEmoji"></span>
- <gl-popover placement="bottom" target="canary-emoji" content="Canary" />
- <span
- id="host-emoji"
- v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('computer')"
- ></span>
- <gl-popover
- placement="bottom"
- target="host-emoji"
- :content="currentRequest.details.host.hostname"
- />
- </span>
+ <gl-link
+ class="gl-text-decoration-underline"
+ :href="currentRequest.details.tracing.tracing_url"
+ >{{ s__('PerformanceBar|Trace') }}</gl-link
+ >
+ </div>
+ <div v-if="showFlamegraphButtons" id="peek-flamegraph" class="view">
+ <gl-link
+ v-gl-tooltip.viewport
+ class="gl-font-sm"
+ :href="flamegraphPath('wall', currentRequestId)"
+ :title="s__('PerformanceBar|Wall flamegraph')"
+ >{{ s__('PerformanceBar|Wall') }}</gl-link
+ >
+ /
+ <gl-link
+ v-gl-tooltip.viewport
+ class="gl-font-sm"
+ :href="flamegraphPath('cpu', currentRequestId)"
+ :title="s__('PerformanceBar|CPU flamegraph')"
+ >{{ s__('PerformanceBar|CPU') }}</gl-link
+ >
+ /
+ <gl-link
+ v-gl-tooltip.viewport
+ class="gl-font-sm"
+ :href="flamegraphPath('object', currentRequestId)"
+ :title="s__('PerformanceBar|Object flamegraph')"
+ >{{ s__('PerformanceBar|Object') }}</gl-link
+ >
+ <span class="gl-opacity-7">{{ s__('PerformanceBar|flamegraph') }}</span>
+ </div>
</div>
- <detailed-metric
- v-for="metric in $options.detailedMetrics"
- :key="metric.metric"
- :current-request="currentRequest"
- :metric="metric.metric"
- :title="metric.title"
- :header="metric.header"
- :keys="metric.keys"
- />
- <div
- v-if="currentRequest.details && currentRequest.details.tracing"
- id="peek-view-trace"
- class="view"
- >
- <gl-link class="gl-text-blue-200" :href="currentRequest.details.tracing.tracing_url">{{
- s__('PerformanceBar|Trace')
- }}</gl-link>
+ <div class="gl-display-flex gl-flex-shrink-0 gl-ml-auto">
+ <div class="gl-display-flex view-reports-container">
+ <gl-link
+ v-if="currentRequest.details"
+ id="peek-download"
+ v-gl-tooltip.viewport
+ class="view gl-font-sm"
+ is-unsafe-link
+ :download="downloadName"
+ :href="downloadPath"
+ :title="s__('PerformanceBar|Download report')"
+ >{{ s__('PerformanceBar|Download') }}</gl-link
+ >
+ <gl-link
+ v-if="showMemoryReportButton"
+ id="peek-memory-report"
+ v-gl-tooltip.viewport
+ class="view gl-font-sm"
+ :href="memoryReportPath"
+ :title="s__('PerformanceBar|Download memory report')"
+ >{{ s__('PerformanceBar|Memory report') }}</gl-link
+ >
+ <gl-link
+ v-if="statsUrl"
+ v-gl-tooltip.viewport
+ class="view gl-font-sm"
+ :href="statsUrl"
+ :title="s__('PerformanceBar|Show stats')"
+ >{{ s__('PerformanceBar|Stats') }}</gl-link
+ >
+ </div>
+ <request-selector
+ v-if="currentRequest"
+ :current-request="currentRequest"
+ :requests="requests"
+ @change-current-request="changeCurrentRequest"
+ />
+ <add-request v-on="$listeners" />
</div>
- <div v-if="currentRequest.details" id="peek-download" class="view">
- <gl-link
- class="gl-text-blue-200"
- is-unsafe-link
- :download="downloadName"
- :href="downloadPath"
- >{{ s__('PerformanceBar|Download') }}</gl-link
- >
- </div>
- <div v-if="showMemoryReportButton" id="peek-memory-report" class="view">
- <gl-link class="gl-text-blue-200" :href="memoryReportPath">{{
- s__('PerformanceBar|Memory report')
- }}</gl-link>
- </div>
- <div v-if="showFlamegraphButtons" id="peek-flamegraph" class="view">
- <span id="flamegraph-emoji" class="gl-text-white-200">
- <span v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('fire')"></span>
- <span v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('bar_chart')"></span>
- </span>
- <gl-popover placement="bottom" target="flamegraph-emoji" content="Flamegraph" />
- <gl-link class="gl-text-blue-200" :href="flamegraphPath('wall', currentRequestId)">{{
- s__('PerformanceBar|wall')
- }}</gl-link>
- /
- <gl-link class="gl-text-blue-200" :href="flamegraphPath('cpu', currentRequestId)">{{
- s__('PerformanceBar|cpu')
- }}</gl-link>
- /
- <gl-link class="gl-text-blue-200" :href="flamegraphPath('object', currentRequestId)">{{
- s__('PerformanceBar|object')
- }}</gl-link>
- </div>
- <gl-link v-if="statsUrl" class="gl-text-blue-200 view" :href="statsUrl">{{
- s__('PerformanceBar|Stats')
- }}</gl-link>
- <request-selector
- v-if="currentRequest"
- :current-request="currentRequest"
- :requests="requests"
- class="gl-ml-auto"
- @change-current-request="changeCurrentRequest"
- />
- <add-request v-on="$listeners" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index f2177e102ec..2914b9762ac 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -1,5 +1,10 @@
<script>
+import { GlFormSelect } from '@gitlab/ui';
+
export default {
+ components: {
+ GlFormSelect,
+ },
props: {
currentRequest: {
type: Object,
@@ -23,8 +28,8 @@ export default {
};
</script>
<template>
- <div id="peek-request-selector" data-qa-selector="request_dropdown" class="view">
- <select v-model="currentRequestId">
+ <div id="peek-request-selector" data-qa-selector="request_dropdown" class="view gl-mr-5">
+ <gl-form-select v-model="currentRequestId" class="gl-px-3! gl-py-2!">
<option
v-for="request in requests"
:key="request.id"
@@ -33,6 +38,6 @@ export default {
>
{{ request.displayName }}
</option>
- </select>
+ </gl-form-select>
</div>
</template>
diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue
index 91e905d62e6..96c11ea9e4e 100644
--- a/app/assets/javascripts/performance_bar/components/request_warning.vue
+++ b/app/assets/javascripts/performance_bar/components/request_warning.vue
@@ -1,14 +1,9 @@
<script>
-import { GlPopover } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { glEmojiTag } from '~/emoji';
+import { GlTooltipDirective } from '@gitlab/ui';
export default {
- components: {
- GlPopover,
- },
directives: {
- SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
props: {
htmlId: {
@@ -32,15 +27,17 @@ export default {
return this.warnings.join('\n');
},
},
- methods: {
- glEmojiTag,
- },
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
<template>
<span v-if="hasWarnings" class="gl-cursor-default">
- <span :id="htmlId" v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('warning')"></span>
- <gl-popover placement="bottom" :target="htmlId" :content="warningMessage" />
+ <gl-emoji
+ v-if="hasWarnings"
+ :id="htmlId"
+ v-gl-tooltip.viewport="warningMessage"
+ data-name="warning"
+ class="gl-ml-2"
+ />
</span>
</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
index ccecc914cf1..0feaf8db82b 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
@@ -1,6 +1,6 @@
<script>
-import { GlLoadingIcon, GlLink } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { createAlert } from '~/alert';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import getLatestPipelineStatusQuery from '../graphql/queries/get_latest_pipeline_status.query.graphql';
@@ -9,9 +9,8 @@ import { COMMIT_BOX_POLL_INTERVAL, PIPELINE_STATUS_FETCH_ERROR } from '../consta
export default {
PIPELINE_STATUS_FETCH_ERROR,
components: {
- CiIcon,
+ CiBadgeLink,
GlLoadingIcon,
- GlLink,
},
inject: {
fullPath: {
@@ -64,8 +63,12 @@ export default {
<template>
<div class="gl-display-inline-block gl-vertical-align-middle gl-mr-2">
<gl-loading-icon v-if="loading" />
- <gl-link v-else :href="pipelineStatus.detailsPath">
- <ci-icon :status="pipelineStatus" :size="24" />
- </gl-link>
+ <ci-badge-link
+ v-else
+ :status="pipelineStatus"
+ :details-path="pipelineStatus.detailsPath"
+ size="md"
+ :show-text="false"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue
index c749034d2a8..b290b2b085f 100644
--- a/app/assets/javascripts/projects/components/shared/delete_button.vue
+++ b/app/assets/javascripts/projects/components/shared/delete_button.vue
@@ -87,7 +87,7 @@ export default {
<gl-button
category="primary"
variant="danger"
- data-qa-selector="delete_button"
+ data-testid="delete-button"
@click="onButtonClick"
>{{ $options.i18n.deleteProject }}</gl-button
>
diff --git a/app/assets/javascripts/projects/project_find_file.js b/app/assets/javascripts/projects/project_find_file.js
index a8b884a68a0..711a8278e07 100644
--- a/app/assets/javascripts/projects/project_find_file.js
+++ b/app/assets/javascripts/projects/project_find_file.js
@@ -50,8 +50,6 @@ export default class ProjectFindFile {
this.initEvent();
// focus text input box
this.inputElement.focus();
- // load file list
- this.load(this.options.url);
}
initEvent() {
@@ -110,7 +108,14 @@ export default class ProjectFindFile {
if (searchText) {
matches = fuzzaldrinPlus.match(filePath, searchText);
}
- const blobItemUrl = joinPaths(this.options.blobUrlTemplate, escapeFileUrl(filePath));
+
+ let blobItemUrl = joinPaths(this.options.blobUrlTemplate, escapeFileUrl(filePath));
+
+ if (this.options.refType) {
+ const blobUrlObject = new URL(blobItemUrl, window.location.origin);
+ blobUrlObject.searchParams.append('ref_type', this.options.refType);
+ blobItemUrl = blobUrlObject.toString();
+ }
const html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find('.tree-table > tbody').append(html));
}
diff --git a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
index b886bf43b57..df99aac6b9e 100644
--- a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
@@ -1,9 +1,7 @@
-import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
-import { ACCESS_LEVEL_DEVELOPER_INTEGER } from '~/access_level/constants';
-const GROUPS_PATH = '/-/autocomplete/project_groups.json';
const USERS_PATH = '/-/autocomplete/users.json';
+const GROUPS_PATH = '/-/autocomplete/project_groups.json';
const DEPLOY_KEYS_PATH = '/-/autocomplete/deploy_keys_with_owners.json';
const buildUrl = (urlRoot, url) => {
@@ -28,14 +26,10 @@ export const getUsers = (query, states) => {
};
export const getGroups = () => {
- if (gon.current_project_id) {
- return Api.projectGroups(gon.current_project_id, {
- with_shared: true,
- shared_min_access_level: ACCESS_LEVEL_DEVELOPER_INTEGER,
- });
- }
- return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH)).then(({ data }) => {
- return data;
+ return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH), {
+ params: {
+ project_id: gon.current_project_id,
+ },
});
};
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index ca24e948f69..2dd7633e2c8 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -229,10 +229,10 @@ export default {
Promise.all([
getDeployKeys(this.query),
getUsers(this.query),
- this.groups.length ? Promise.resolve(this.groups) : getGroups(),
+ this.groups.length ? Promise.resolve({ data: this.groups }) : getGroups(),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse);
+ this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data);
this.setSelected({ initial });
})
.catch(() =>
diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
index fd5fabd7c8a..a426d6d7bb8 100644
--- a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
+++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
@@ -51,7 +51,7 @@ export default {
:disabled="!hasSelectedNamespace"
:phrase="confirmationPhrase"
:button-text="confirmButtonText"
- button-qa-selector="transfer_project_button"
+ button-testid="transfer-project-button"
@confirm="$emit('confirm')"
/>
</div>
diff --git a/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue b/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue
new file mode 100644
index 00000000000..d17144669fe
--- /dev/null
+++ b/app/assets/javascripts/ref/components/ambiguous_ref_modal.vue
@@ -0,0 +1,80 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script>
+import { GlModal, GlButton, GlSprintf } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { REF_TYPE_PARAM_NAME, TAG_REF_TYPE, BRANCH_REF_TYPE } from '../constants';
+
+export default {
+ i18n: {
+ title: s__('AmbiguousRef|Which reference do you want to view?'),
+ description: sprintf(
+ s__('AmbiguousRef|There is a branch and a tag with the same name of %{ref}.'),
+ ),
+ secondaryDescription: s__('AmbiguousRef|Which reference would you like to view?'),
+ viewTagButton: s__('AmbiguousRef|View tag'),
+ viewBranchButton: s__('AmbiguousRef|View branch'),
+ },
+ tagRefType: TAG_REF_TYPE,
+ branchRefType: BRANCH_REF_TYPE,
+ components: {
+ GlModal,
+ GlButton,
+ GlSprintf,
+ },
+
+ props: {
+ refName: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ this.$refs.ambiguousRefModal.show();
+ },
+ methods: {
+ navigate(refType) {
+ const url = new URL(window.location.href);
+ url.searchParams.set(REF_TYPE_PARAM_NAME, refType);
+
+ visitUrl(url.toString());
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="ambiguousRefModal"
+ modal-id="ambiguous-ref"
+ :title="$options.i18n.title"
+ @primary="navigate"
+ >
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.i18n.description">
+ <template #ref
+ ><code>{{ refName }}</code></template
+ >
+ </gl-sprintf>
+ </p>
+
+ <p>
+ {{ $options.i18n.secondaryDescription }}
+ </p>
+
+ <template #modal-footer>
+ <gl-button
+ category="secondary"
+ variant="confirm"
+ @click="() => navigate($options.tagRefType)"
+ >{{ $options.i18n.viewTagButton }}</gl-button
+ >
+ <gl-button
+ category="secondary"
+ variant="confirm"
+ @click="() => navigate($options.branchRefType)"
+ >{{ $options.i18n.viewBranchButton }}</gl-button
+ >
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index e5f5800c99c..ed9fd521e67 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -11,6 +11,10 @@ import {
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
+ TAG_REF_TYPE,
+ BRANCH_REF_TYPE,
+ TAG_REF_TYPE_ICON,
+ BRANCH_REF_TYPE_ICON,
} from '../constants';
import createStore from '../stores';
import { formatListBoxItems, formatErrors } from '../format_refs';
@@ -159,6 +163,17 @@ export default {
})
: this.i18n.noResults;
},
+ dropdownIcon() {
+ let icon;
+
+ if (this.selectedRef.includes(`refs/${TAG_REF_TYPE}`)) {
+ icon = TAG_REF_TYPE_ICON;
+ } else if (this.selectedRef.includes(`refs/${BRANCH_REF_TYPE}`)) {
+ icon = BRANCH_REF_TYPE_ICON;
+ }
+
+ return icon;
+ },
},
watch: {
// Keep the Vuex store synchronized if the parent
@@ -246,6 +261,7 @@ export default {
:search-placeholder="i18n.searchPlaceholder"
:toggle-class="extendedToggleButtonClass"
:toggle-text="buttonText"
+ :icon="dropdownIcon"
v-bind="$attrs"
v-on="$listeners"
@hidden="$emit('hide')"
diff --git a/app/assets/javascripts/ref/constants.js b/app/assets/javascripts/ref/constants.js
index 4b5b18cf6c1..5fd4660b8e3 100644
--- a/app/assets/javascripts/ref/constants.js
+++ b/app/assets/javascripts/ref/constants.js
@@ -7,6 +7,9 @@ export const REF_TYPE_COMMITS = 'REF_TYPE_COMMITS';
export const ALL_REF_TYPES = Object.freeze([REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS]);
export const BRANCH_REF_TYPE = 'heads';
export const TAG_REF_TYPE = 'tags';
+export const TAG_REF_TYPE_ICON = 'tag';
+export const BRANCH_REF_TYPE_ICON = 'branch';
+export const REF_TYPE_PARAM_NAME = 'ref_type';
export const X_TOTAL_HEADER = 'x-total';
diff --git a/app/assets/javascripts/ref/init_ambiguous_ref_modal.js b/app/assets/javascripts/ref/init_ambiguous_ref_modal.js
new file mode 100644
index 00000000000..00fb8f10401
--- /dev/null
+++ b/app/assets/javascripts/ref/init_ambiguous_ref_modal.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
+import AmbiguousRefModal from './components/ambiguous_ref_modal.vue';
+import { REF_TYPE_PARAM_NAME, TAG_REF_TYPE, BRANCH_REF_TYPE } from './constants';
+
+export default (el = document.querySelector('#js-ambiguous-ref-modal')) => {
+ const refType = getParameterByName(REF_TYPE_PARAM_NAME);
+ const isRefTypeSet = refType === TAG_REF_TYPE || refType === BRANCH_REF_TYPE; // if ref_type is already set in the URL, we don't want to display the modal
+ if (!el || isRefTypeSet || !parseBoolean(el.dataset.ambiguous)) return false;
+
+ const { ref } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(AmbiguousRefModal, { props: { refName: ref } });
+ },
+ });
+};
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 d36b29f69a5..a0e876b4c19 100644
--- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -219,7 +219,7 @@ export default {
type="submit"
size="small"
class="gl-mr-2"
- data-testid="add_issue_button"
+ data-testid="add-issue-button"
>
{{ __('Add') }}
</gl-button>
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 f92c81a7eb2..4811dfef3d0 100644
--- a/app/assets/javascripts/related_issues/components/related_issuable_input.vue
+++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
@@ -217,7 +217,7 @@ export default {
:aria-label="inputPlaceholder"
type="text"
class="gl-w-full gl-border-none gl-outline-0"
- data-testid="add_issue_field"
+ data-testid="add-issue-field"
autocomplete="off"
@input="onInput"
@focus="onFocus"
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 1044d25c1a3..f1b6b335509 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_block.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -278,20 +278,17 @@ export default {
@saveReorder="$emit('saveReorder', $event)"
/>
</template>
- <div v-if="!shouldShowTokenBody && !isFormVisible">
- <p class="gl-new-card-empty">
- {{ emptyStateMessage }}
- <gl-link
- v-if="hasHelpPath"
- :href="helpPath"
- target="_blank"
- data-testid="help-link"
- :aria-label="helpLinkText"
- >
- {{ __('Learn more.') }}
- </gl-link>
- </p>
- </div>
+ <p v-if="!shouldShowTokenBody && !isFormVisible" class="gl-new-card-empty">
+ {{ emptyStateMessage }}
+ <gl-link
+ v-if="hasHelpPath"
+ :href="helpPath"
+ data-testid="help-link"
+ :aria-label="helpLinkText"
+ >
+ {{ __('Learn more.') }}
+ </gl-link>
+ </p>
</div>
</gl-card>
</div>
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 8d26917f749..0e47184e24e 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_list.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue
@@ -104,7 +104,7 @@ export default {
{{ heading }}
</h4>
<div class="related-issues-token-body" :class="{ 'sortable-container': canReorder }">
- <div v-if="isFetching" class="gl-mb-2" data-testid="related_issues_loading_placeholder">
+ <div v-if="isFetching" class="gl-mb-2" data-testid="related-issues-loading-placeholder">
<gl-loading-icon
ref="loadingIcon"
size="sm"
@@ -146,7 +146,7 @@ export default {
:locked-message="issue.lockedMessage"
:work-item-type="issue.type"
event-namespace="relatedIssue"
- data-testid="related_issuable_content"
+ data-testid="related-issuable-content"
class="gl-mx-n2"
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
/>
diff --git a/app/assets/javascripts/releases/components/releases_empty_state.vue b/app/assets/javascripts/releases/components/releases_empty_state.vue
index ae94bd6872e..2893a42c73b 100644
--- a/app/assets/javascripts/releases/components/releases_empty_state.vue
+++ b/app/assets/javascripts/releases/components/releases_empty_state.vue
@@ -21,10 +21,11 @@ export default {
</script>
<template>
<gl-empty-state
- class="gl-layout-w-limited"
+ class="gl-layout-w-limited gl-mx-auto"
:title="$options.i18n.emptyStateTitle"
:description="$options.i18n.emptyStateText"
:svg-path="illustrationPath"
+ :svg-height="null"
:primary-button-link="newReleasePath"
:primary-button-text="$options.i18n.newRelease"
:secondary-button-link="documentationPath"
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index fe996a2a734..04f3d73235b 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -1,15 +1,18 @@
<script>
-import { GlDropdown, GlFormGroup, GlPopover } from '@gitlab/ui';
+import { GlButton, GlTruncate, GlIcon, GlFormGroup, GlPopover } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions, mapGetters } from 'vuex';
import { __, s__ } from '~/locale';
+import { ESC_KEY } from '~/lib/utils/keys';
import TagSearch from './tag_search.vue';
import TagCreate from './tag_create.vue';
export default {
components: {
- GlDropdown,
+ GlTruncate,
+ GlButton,
+ GlIcon,
GlFormGroup,
GlPopover,
TagSearch,
@@ -74,6 +77,13 @@ export default {
},
hidePopover() {
this.show = false;
+ // gl-button doesn't expose focus method, but we can find button element by id
+ document.getElementById(this.id)?.focus();
+ },
+ onPopoverKeyUp(e) {
+ if (e.code === ESC_KEY) {
+ this.hidePopover();
+ }
},
},
i18n: {
@@ -97,15 +107,12 @@ export default {
:invalid-feedback="tagFeedback"
optional
>
- <gl-dropdown
- :id="id"
- :variant="buttonVariant"
- :text="buttonText"
- :toggle-class="['gl-text-gray-900!']"
- category="secondary"
- class="gl-w-30"
- @show.prevent="showPopover"
- />
+ <gl-button :id="id" class="gl-w-30 gl-px-0!" @click="showPopover">
+ <span class="gl-w-28 gl-display-flex gl-justify-content-space-between">
+ <gl-truncate :text="buttonText" class="gl-max-w-26" />
+ <gl-icon class="gl-button-icon gl-new-dropdown-chevron" name="chevron-down" />
+ </span>
+ </gl-button>
<gl-popover
:show="show"
:target="id"
@@ -118,7 +125,7 @@ export default {
@close-button-clicked="hidePopover"
@hide.once="markInputAsDirty"
>
- <div class="gl-border-t-solid gl-border-t-1 gl-border-gray-200">
+ <div class="gl-border-t-solid gl-border-t-1 gl-border-gray-200" @keyup="onPopoverKeyUp">
<tag-create
v-if="isCreating"
v-model="newTagName"
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
index 2e3cf3bf9b8..8bdfb057adc 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -1,6 +1,8 @@
+import { omit } from 'lodash';
import { getTag } from '~/rest_api';
import { createAlert } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import AccessorUtilities from '~/lib/utils/accessor';
import { s__ } from '~/locale';
import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
import deleteReleaseMutation from '~/releases/graphql/mutations/delete_release.mutation.graphql';
@@ -15,16 +17,26 @@ import * as types from './mutation_types';
class GraphQLError extends Error {}
-export const initializeRelease = ({ commit, dispatch, state }) => {
+const updateDraft = (action) => (store, ...args) => {
+ action(store, ...args);
+
+ if (!store.state.isExistingRelease) {
+ store.dispatch('saveDraftRelease');
+ store.dispatch('saveDraftCreateFrom');
+ }
+};
+
+export const initializeRelease = ({ dispatch, state }) => {
if (state.isExistingRelease) {
// When editing an existing release,
// fetch the release object from the API
return dispatch('fetchRelease');
}
- // When creating a new release, initialize the
- // store with an empty release object
- commit(types.INITIALIZE_EMPTY_RELEASE);
+ // When creating a new release, try to load the
+ // store with a draft release object, otherwise
+ // initialize an empty one
+ dispatch('loadDraftRelease');
return Promise.resolve();
};
@@ -51,50 +63,58 @@ export const fetchRelease = async ({ commit, state }) => {
}
};
-export const updateReleaseTagName = ({ commit }, tagName) =>
- commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
+export const updateReleaseTagName = updateDraft(({ commit }, tagName) =>
+ commit(types.UPDATE_RELEASE_TAG_NAME, tagName),
+);
-export const updateReleaseTagMessage = ({ commit }, tagMessage) =>
- commit(types.UPDATE_RELEASE_TAG_MESSAGE, tagMessage);
+export const updateReleaseTagMessage = updateDraft(({ commit }, tagMessage) =>
+ commit(types.UPDATE_RELEASE_TAG_MESSAGE, tagMessage),
+);
-export const updateCreateFrom = ({ commit }, createFrom) =>
- commit(types.UPDATE_CREATE_FROM, createFrom);
+export const updateCreateFrom = updateDraft(({ commit }, createFrom) =>
+ commit(types.UPDATE_CREATE_FROM, createFrom),
+);
export const updateShowCreateFrom = ({ commit }, showCreateFrom) =>
commit(types.UPDATE_SHOW_CREATE_FROM, showCreateFrom);
-export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
+export const updateReleaseTitle = updateDraft(({ commit }, title) =>
+ commit(types.UPDATE_RELEASE_TITLE, title),
+);
-export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
+export const updateReleaseNotes = updateDraft(({ commit }, notes) =>
+ commit(types.UPDATE_RELEASE_NOTES, notes),
+);
-export const updateReleaseMilestones = ({ commit }, milestones) =>
- commit(types.UPDATE_RELEASE_MILESTONES, milestones);
+export const updateReleaseMilestones = updateDraft(({ commit }, milestones) =>
+ commit(types.UPDATE_RELEASE_MILESTONES, milestones),
+);
-export const updateReleaseGroupMilestones = ({ commit }, groupMilestones) =>
- commit(types.UPDATE_RELEASE_GROUP_MILESTONES, groupMilestones);
+export const updateReleaseGroupMilestones = updateDraft(({ commit }, groupMilestones) =>
+ commit(types.UPDATE_RELEASE_GROUP_MILESTONES, groupMilestones),
+);
-export const addEmptyAssetLink = ({ commit }) => {
- commit(types.ADD_EMPTY_ASSET_LINK);
-};
+export const addEmptyAssetLink = updateDraft(({ commit }) => commit(types.ADD_EMPTY_ASSET_LINK));
-export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
- commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
-};
+export const updateAssetLinkUrl = updateDraft(({ commit }, { linkIdToUpdate, newUrl }) =>
+ commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl }),
+);
-export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
- commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
-};
+export const updateAssetLinkName = updateDraft(({ commit }, { linkIdToUpdate, newName }) =>
+ commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName }),
+);
-export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) => {
- commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType });
-};
+export const updateAssetLinkType = updateDraft(({ commit }, { linkIdToUpdate, newType }) =>
+ commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType }),
+);
-export const removeAssetLink = ({ commit }, linkIdToRemove) => {
- commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
-};
+export const removeAssetLink = updateDraft(({ commit }, linkIdToRemove) =>
+ commit(types.REMOVE_ASSET_LINK, linkIdToRemove),
+);
-export const receiveSaveReleaseSuccess = ({ commit }, urlToRedirectTo) => {
+export const receiveSaveReleaseSuccess = ({ commit, dispatch }, urlToRedirectTo) => {
commit(types.RECEIVE_SAVE_RELEASE_SUCCESS);
+ dispatch('clearDraftRelease');
redirectTo(urlToRedirectTo); // eslint-disable-line import/no-deprecated
};
@@ -245,9 +265,9 @@ export const updateIncludeTagNotes = ({ commit }, includeTagNotes) => {
commit(types.UPDATE_INCLUDE_TAG_NOTES, includeTagNotes);
};
-export const updateReleasedAt = ({ commit }, releasedAt) => {
- commit(types.UPDATE_RELEASED_AT, releasedAt);
-};
+export const updateReleasedAt = updateDraft(({ commit }, releasedAt) =>
+ commit(types.UPDATE_RELEASED_AT, releasedAt),
+);
export const deleteRelease = ({ commit, getters, dispatch, state }) => {
commit(types.REQUEST_SAVE_RELEASE);
@@ -274,3 +294,56 @@ export const setCreating = ({ commit }) => commit(types.SET_CREATING);
export const setExistingTag = ({ commit }) => commit(types.SET_EXISTING_TAG);
export const setNewTag = ({ commit }) => commit(types.SET_NEW_TAG);
+
+export const saveDraftRelease = ({ getters, state }) => {
+ try {
+ window.localStorage.setItem(
+ getters.localStorageKey,
+ JSON.stringify(getters.releasedAtChanged ? state.release : omit(state.release, 'releasedAt')),
+ );
+ } catch {
+ return Promise.resolve();
+ }
+ return Promise.resolve();
+};
+
+export const saveDraftCreateFrom = ({ getters, state }) => {
+ try {
+ window.localStorage.setItem(
+ getters.localStorageCreateFromKey,
+ JSON.stringify(state.createFrom),
+ );
+ } catch {
+ return Promise.resolve();
+ }
+ return Promise.resolve();
+};
+
+export const clearDraftRelease = ({ getters }) => {
+ if (AccessorUtilities.canUseLocalStorage()) {
+ window.localStorage.removeItem(getters.localStorageKey);
+ window.localStorage.removeItem(getters.localStorageCreateFromKey);
+ }
+};
+
+export const loadDraftRelease = ({ commit, getters, state }) => {
+ try {
+ const release = window.localStorage.getItem(getters.localStorageKey);
+ const createFrom = window.localStorage.getItem(getters.localStorageCreateFromKey);
+
+ if (release) {
+ const parsedRelease = JSON.parse(release);
+ commit(types.INITIALIZE_RELEASE, {
+ ...parsedRelease,
+ releasedAt: parsedRelease.releasedAt
+ ? new Date(parsedRelease.releasedAt)
+ : state.originalReleasedAt,
+ });
+ commit(types.UPDATE_CREATE_FROM, JSON.parse(createFrom));
+ } else {
+ commit(types.INITIALIZE_EMPTY_RELEASE);
+ }
+ } catch {
+ commit(types.INITIALIZE_EMPTY_RELEASE);
+ }
+};
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
index edf6c81c9e9..0b37c2b81d1 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
@@ -190,3 +190,7 @@ export const isCreating = ({ step }) => step === CREATE;
export const isExistingTag = ({ tagStep }) => tagStep === EXISTING_TAG;
export const isNewTag = ({ tagStep }) => tagStep === NEW_TAG;
+
+export const localStorageKey = ({ projectPath }) => `${projectPath}/release/new`;
+export const localStorageCreateFromKey = ({ projectPath }) =>
+ `${projectPath}/release/new/createFrom`;
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
index fc450970cde..8a0eeaa4338 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
@@ -1,4 +1,5 @@
export const INITIALIZE_EMPTY_RELEASE = 'INITIALIZE_EMPTY_RELEASE';
+export const INITIALIZE_RELEASE = 'INITIALIZE_RELEASE';
export const REQUEST_RELEASE = 'REQUEST_RELEASE';
export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
index 7ff18245a80..3a68cdbb89a 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
@@ -22,6 +22,9 @@ export default {
},
};
},
+ [types.INITIALIZE_RELEASE](state, release) {
+ state.release = release;
+ },
[types.REQUEST_RELEASE](state) {
state.isFetchingRelease = true;
diff --git a/app/assets/javascripts/repository/components/blob_controls.vue b/app/assets/javascripts/repository/components/blob_controls.vue
index 460db0fe2ae..4730c9575da 100644
--- a/app/assets/javascripts/repository/components/blob_controls.vue
+++ b/app/assets/javascripts/repository/components/blob_controls.vue
@@ -30,6 +30,7 @@ export default {
projectPath: this.projectPath,
filePath: this.filePath,
ref: this.ref,
+ refType: this.refType?.toUpperCase(),
};
},
skip() {
@@ -45,6 +46,11 @@ export default {
type: String,
required: true,
},
+ refType: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index b347f97a5ae..e3cd2d2e842 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -184,7 +184,7 @@ export default {
this.currentPath ? encodeURIComponent(this.currentPath) : '',
),
extraAttrs: {
- 'data-qa-selector': 'new_file_menu_item',
+ 'data-testid': 'new-file-menu-item',
},
},
{
@@ -284,7 +284,6 @@ export default {
:toggle-text="__('Add to tree')"
toggle-class="add-to-tree gl-ml-2"
data-testid="add-to-tree"
- data-qa-selector="add_to_tree_dropdown"
text-sr-only
icon="plus"
:items="dropdownItems"
diff --git a/app/assets/javascripts/repository/components/commit_info.vue b/app/assets/javascripts/repository/components/commit_info.vue
new file mode 100644
index 00000000000..b6e3cdbb7a3
--- /dev/null
+++ b/app/assets/javascripts/repository/components/commit_info.vue
@@ -0,0 +1,116 @@
+<script>
+import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import defaultAvatarUrl from 'images/no_avatar.png';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import getRefMixin from '../mixins/get_ref';
+
+export default {
+ components: {
+ UserAvatarLink,
+ TimeagoTooltip,
+ GlButton,
+ GlLink,
+ UserAvatarImage,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
+ },
+ mixins: [getRefMixin],
+ props: {
+ commit: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return { showDescription: false };
+ },
+ computed: {
+ commitDescription() {
+ // Strip the newline at the beginning
+ return this.commit?.descriptionHtml?.replace(/^&#x000A;/, '');
+ },
+ },
+ methods: {
+ toggleShowDescription() {
+ this.showDescription = !this.showDescription;
+ },
+ },
+ defaultAvatarUrl,
+ safeHtmlConfig: {
+ ADD_TAGS: ['gl-emoji'],
+ },
+ i18n: {
+ toggleCommitDescription: __('Toggle commit description'),
+ authored: __('authored'),
+ },
+};
+</script>
+
+<template>
+ <div class="well-segment commit gl-min-h-8 gl-p-2 gl-w-full gl-display-flex">
+ <user-avatar-link
+ v-if="commit.author"
+ :link-href="commit.author.webPath"
+ :img-src="commit.author.avatarUrl"
+ :img-size="32"
+ class="gl-my-2 gl-mr-4"
+ />
+ <user-avatar-image
+ v-else
+ class="gl-my-2 gl-mr-4"
+ :img-src="commit.authorGravatar || $options.defaultAvatarUrl"
+ :size="32"
+ />
+ <div
+ class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
+ >
+ <div class="commit-content" data-qa-selector="commit_content">
+ <gl-link
+ v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
+ :href="commit.webPath"
+ :class="{ 'gl-font-style-italic': !commit.message }"
+ class="commit-row-message item-title"
+ />
+ <gl-button
+ v-if="commit.descriptionHtml"
+ v-gl-tooltip
+ :class="{ open: showDescription }"
+ :title="$options.i18n.toggleCommitDescription"
+ :aria-label="$options.i18n.toggleCommitDescription"
+ :selected="showDescription"
+ class="text-expander gl-vertical-align-bottom!"
+ icon="ellipsis_h"
+ @click="toggleShowDescription"
+ />
+ <div class="committer">
+ <gl-link
+ v-if="commit.author"
+ :href="commit.author.webPath"
+ class="commit-author-link js-user-link"
+ >
+ {{ commit.author.name }}</gl-link
+ >
+ <template v-else>
+ {{ commit.authorName }}
+ </template>
+ {{ $options.i18n.authored }}
+ <timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" />
+ </div>
+ <pre
+ v-if="commitDescription"
+ v-safe-html:[$options.safeHtmlConfig]="commitDescription"
+ :class="{ 'gl-display-block!': showDescription }"
+ class="commit-row-description gl-mb-3 gl-white-space-pre-line"
+ ></pre>
+ </div>
+ <div class="gl-flex-grow-1"></div>
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/fork_info.vue b/app/assets/javascripts/repository/components/fork_info.vue
index 42108e8dfba..c0adbc6f38c 100644
--- a/app/assets/javascripts/repository/components/fork_info.vue
+++ b/app/assets/javascripts/repository/components/fork_info.vue
@@ -291,7 +291,7 @@ export default {
>
<div v-if="sourceName">
{{ $options.i18n.forkedFrom }}
- <gl-link data-qa-selector="forked_from_link" :href="sourcePath">{{ sourceName }}</gl-link>
+ <gl-link data-testid="forked-from-link" :href="sourcePath">{{ sourceName }}</gl-link>
<gl-skeleton-loader v-if="isLoading" :lines="1" />
<div v-else class="gl-text-secondary" data-testid="divergence-message">
<gl-sprintf :message="forkDivergenceMessage">
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 12edeeb0d2f..05d4d9e1f81 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -1,32 +1,26 @@
<script>
-import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import defaultAvatarUrl from 'images/no_avatar.png';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.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 UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import SignatureBadge from '~/commit/components/signature_badge.vue';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
import eventHub from '../event_hub';
import { FORK_UPDATED_EVENT } from '../constants';
+import CommitInfo from './commit_info.vue';
export default {
components: {
- UserAvatarLink,
- TimeagoTooltip,
+ CommitInfo,
ClipboardButton,
- GlButton,
- GlButtonGroup,
- GlLink,
- GlLoadingIcon,
- UserAvatarImage,
SignatureBadge,
CiBadgeLink,
+ GlButtonGroup,
+ GlButton,
+ GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -80,13 +74,12 @@ export default {
return {
projectPath: '',
commit: null,
- showDescription: false,
};
},
computed: {
statusTitle() {
return sprintf(s__('PipelineStatusTooltip|Pipeline: %{ciStatus}'), {
- ciStatus: this.commit.pipeline.detailedStatus.text,
+ ciStatus: this.commit?.pipeline?.detailedStatus?.text,
});
},
isLoading() {
@@ -95,10 +88,6 @@ export default {
showCommitId() {
return this.commit?.sha?.substr(0, 8);
},
- commitDescription() {
- // Strip the newline at the beginning
- return this.commit?.descriptionHtml?.replace(/^&#x000A;/, '');
- },
},
watch: {
currentPath() {
@@ -112,112 +101,39 @@ export default {
eventHub.$off(FORK_UPDATED_EVENT, this.refetchLastCommit);
},
methods: {
- toggleShowDescription() {
- this.showDescription = !this.showDescription;
- },
refetchLastCommit() {
this.$apollo.queries.commit.refetch();
},
},
- defaultAvatarUrl,
- safeHtmlConfig: {
- ADD_TAGS: ['gl-emoji'],
- },
};
</script>
<template>
- <div class="well-segment commit gl-p-5 gl-w-full gl-display-flex">
- <gl-loading-icon v-if="isLoading" size="lg" color="dark" class="m-auto" />
- <template v-else-if="commit">
- <user-avatar-link
- v-if="commit.author"
- :link-href="commit.author.webPath"
- :img-src="commit.author.avatarUrl"
- :img-size="32"
- class="gl-my-2 gl-mr-4"
- />
- <user-avatar-image
- v-else
- class="gl-my-2 gl-mr-4"
- :img-src="commit.authorGravatar || $options.defaultAvatarUrl"
- :size="32"
- />
- <div
- class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
- >
- <div class="commit-content" data-qa-selector="commit_content">
- <gl-link
- v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
- :href="commit.webPath"
- :class="{ 'font-italic': !commit.message }"
- class="commit-row-message item-title"
- />
- <gl-button
- v-if="commit.descriptionHtml"
- v-gl-tooltip
- :class="{ open: showDescription }"
- :title="__('Toggle commit description')"
- :aria-label="__('Toggle commit description')"
- :selected="showDescription"
- class="text-expander gl-vertical-align-bottom!"
- icon="ellipsis_h"
- @click="toggleShowDescription"
- />
- <div class="committer">
- <gl-link
- v-if="commit.author"
- :href="commit.author.webPath"
- class="commit-author-link js-user-link"
- >
- {{ commit.author.name }}</gl-link
- >
- <template v-else>
- {{ commit.authorName }}
- </template>
- {{ s__('LastCommit|authored') }}
- <timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" />
- </div>
- <pre
- v-if="commitDescription"
- v-safe-html:[$options.safeHtmlConfig]="commitDescription"
- :class="{ 'd-block': showDescription }"
- class="commit-row-description gl-mb-3 gl-white-space-pre-line"
- ></pre>
- </div>
- <div class="gl-flex-grow-1"></div>
- <div
- class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
- >
- <signature-badge v-if="commit.signature" :signature="commit.signature" />
- <div v-if="commit.pipeline" class="ci-status-link">
- <ci-badge-link
- :status="commit.pipeline.detailedStatus"
- :details-path="commit.pipeline.detailedStatus.detailsPath"
- :aria-label="statusTitle"
- size="lg"
- :show-text="false"
- class="js-commit-pipeline"
- />
- </div>
- <gl-button-group class="gl-ml-4 js-commit-sha-group">
- <gl-button label class="gl-font-monospace" data-testid="last-commit-id-label">{{
- showCommitId
- }}</gl-button>
- <clipboard-button
- :text="commit.sha"
- :title="__('Copy commit SHA')"
- class="input-group-text"
- />
- </gl-button-group>
- </div>
+ <gl-loading-icon v-if="isLoading" size="lg" color="dark" class="m-auto" />
+ <commit-info v-else-if="commit" :commit="commit">
+ <div
+ class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
+ >
+ <signature-badge v-if="commit.signature" :signature="commit.signature" />
+ <div v-if="commit.pipeline" class="ci-status-link">
+ <ci-badge-link
+ :status="commit.pipeline.detailedStatus"
+ :details-path="commit.pipeline.detailedStatus.detailsPath"
+ :aria-label="statusTitle"
+ :show-text="false"
+ class="js-commit-pipeline"
+ />
</div>
- </template>
- </div>
+ <gl-button-group class="gl-ml-4 js-commit-sha-group">
+ <gl-button label class="gl-font-monospace" data-testid="last-commit-id-label">{{
+ showCommitId
+ }}</gl-button>
+ <clipboard-button
+ :text="commit.sha"
+ :title="__('Copy commit SHA')"
+ class="input-group-text"
+ />
+ </gl-button-group>
+ </div>
+ </commit-info>
</template>
-
-<style scoped>
-.commit {
- min-height: 4.75rem;
-}
-</style>
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index bdcacd80b30..be446260f82 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -67,7 +67,7 @@ export default {
</gl-link>
</div>
</div>
- <div class="blob-viewer" data-qa-selector="blob_viewer_content" itemprop="about">
+ <div class="blob-viewer" data-testid="blob-viewer-content" itemprop="about">
<gl-loading-icon v-if="isLoading" size="lg" color="dark" class="my-4 mx-auto" />
<div
v-else-if="readme"
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 557e9cd168f..3da7daa3eec 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -118,7 +118,7 @@ export default {
class="table tree-table"
:class="{ 'gl-table-layout-fixed': !showParentRow }"
aria-live="polite"
- data-qa-selector="file_tree_table"
+ data-testid="file-tree-table"
>
<table-header v-once />
<tbody>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index a76d822317a..526757e6147 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -219,7 +219,7 @@ export default {
'is-submodule': isSubmodule,
}"
class="tree-item-link str-truncated"
- data-qa-selector="file_name_link"
+ data-testid="file-name-link"
>
<file-icon
:file-name="fullPath"
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 9753173ac30..afe3f7b1983 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -122,7 +122,7 @@ export default function setupVueRepositoryList() {
return h(LastCommit, {
props: {
currentPath: this.$route.params.path,
- refType: this.$route.query.ref_type,
+ refType: this.$route.meta.refType || this.$route.query.ref_type,
},
});
},
@@ -137,6 +137,7 @@ export default function setupVueRepositoryList() {
return h(BlobControls, {
props: {
projectPath,
+ refType: this.$route.meta.refType || this.$route.query.ref_type,
},
});
},
@@ -231,19 +232,21 @@ export default function setupVueRepositoryList() {
const treeHistoryLinkEl = document.getElementById('js-tree-history-link');
const { historyLink } = treeHistoryLinkEl.dataset;
-
// eslint-disable-next-line no-new
new Vue({
el: treeHistoryLinkEl,
router,
render(h) {
+ const url = new URL(window.location.href);
+ url.pathname = `${historyLink}/${
+ this.$route.params.path ? escapeFileUrl(this.$route.params.path) : ''
+ }`;
+ url.searchParams.set('ref_type', this.$route.meta.refType || this.$route.query.ref_type);
return h(
GlButton,
{
attrs: {
- href: `${historyLink}/${
- this.$route.params.path ? escapeFileUrl(this.$route.params.path) : ''
- }`,
+ href: url.href,
// Ideally passing this class to `props` should work
// But it doesn't work here. :(
class: 'btn btn-default btn-md gl-button',
@@ -256,7 +259,7 @@ export default function setupVueRepositoryList() {
initWebIdeLink({ el: document.getElementById('js-tree-web-ide-link'), router });
- const directoryDownloadLinks = document.getElementById('js-directory-downloads');
+ const directoryDownloadLinks = document.querySelector('.js-directory-downloads');
if (directoryDownloadLinks) {
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/repository/queries/blob_controls.query.graphql b/app/assets/javascripts/repository/queries/blob_controls.query.graphql
index fc1cf5f254b..0c284dcc8e6 100644
--- a/app/assets/javascripts/repository/queries/blob_controls.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_controls.query.graphql
@@ -1,8 +1,8 @@
-query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!) {
+query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!, $refType: RefType) {
project(fullPath: $projectPath) {
id
repository {
- blobs(paths: [$filePath], ref: $ref) {
+ blobs(paths: [$filePath], ref: $ref, refType: $refType) {
nodes {
id
findFilePath
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index 5f73912ed2b..31bafab742d 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -63,6 +63,9 @@ export default function createRouter(base, baseRef) {
props: {
refType: 'HEADS',
},
+ meta: {
+ refType: 'HEADS',
+ },
},
],
});
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 532a66affd8..2ff138cabe5 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -15,6 +15,7 @@ import {
SCOPE_PROJECTS,
SCOPE_NOTES,
SCOPE_COMMITS,
+ SCOPE_MILESTONES,
SEARCH_TYPE_ADVANCED,
} from '../constants';
import IssuesFilters from './issues_filters.vue';
@@ -23,6 +24,7 @@ import BlobsFilters from './blobs_filters.vue';
import ProjectsFilters from './projects_filters.vue';
import NotesFilters from './notes_filters.vue';
import CommitsFilters from './commits_filters.vue';
+import MilestonesFilters from './milestones_filters.vue';
export default {
name: 'GlobalSearchSidebar',
@@ -38,6 +40,7 @@ export default {
DomElementListener,
SmallScreenDrawerNavigation,
CommitsFilters,
+ MilestonesFilters,
},
mixins: [glFeatureFlagsMixin()],
computed: {
@@ -57,18 +60,20 @@ export default {
return this.currentScope === SCOPE_PROJECTS;
},
showNotesFilters() {
- return (
- this.currentScope === SCOPE_NOTES &&
- this.searchType === SEARCH_TYPE_ADVANCED &&
- this.glFeatures.searchNotesHideArchivedProjects
- );
+ // for now, the feature flag is placed here. Since we have only one filter in notes scope
+ return this.currentScope === SCOPE_NOTES && this.glFeatures.searchNotesHideArchivedProjects;
},
showCommitsFilters() {
// for now, the feature flag is placed here. Since we have only one filter in commits scope
return (
- this.currentScope === SCOPE_COMMITS &&
- this.searchType === SEARCH_TYPE_ADVANCED &&
- this.glFeatures.searchCommitsHideArchivedProjects
+ this.currentScope === SCOPE_COMMITS && this.glFeatures.searchCommitsHideArchivedProjects
+ );
+ },
+ showMilestonesFilters() {
+ // for now, the feature flag is placed here. Since we have only one filter in milestones scope
+ return (
+ this.currentScope === SCOPE_MILESTONES &&
+ this.glFeatures.searchMilestonesHideArchivedProjects
);
},
showScopeNavigation() {
@@ -97,6 +102,7 @@ export default {
<projects-filters v-if="showProjectsFilters" />
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
+ <milestones-filters v-if="showMilestonesFilters" />
</sidebar-portal>
</section>
@@ -112,6 +118,7 @@ export default {
<projects-filters v-if="showProjectsFilters" />
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
+ <milestones-filters v-if="showMilestonesFilters" />
</div>
<small-screen-drawer-navigation class="gl-lg-display-none">
<scope-legacy-navigation />
@@ -121,6 +128,7 @@ export default {
<projects-filters v-if="showProjectsFilters" />
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
+ <milestones-filters v-if="showMilestonesFilters" />
</small-screen-drawer-navigation>
</section>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
index 5cddf5e744f..ed90e2aaded 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
@@ -5,14 +5,7 @@ const checkboxLabel = s__('GlobalSearch|Include archived');
export const TRACKING_NAMESPACE = 'search:archived:select';
export const TRACKING_LABEL_CHECKBOX = 'checkbox';
-const scopes = {
- PROJECTS: 'projects',
- ISSUES: 'issues',
- MERGE_REQUESTS: 'merge_requests',
- NOTES: 'notes',
- BLOBS: 'blobs',
- COMMITS: 'commits',
-};
+const scopes = ['projects', 'issues', 'merge_requests', 'notes', 'blobs', 'commits', 'milestones'];
const filterParam = 'include_archived';
diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
index c31c46f2e6a..b0e84beabc4 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
@@ -1,7 +1,8 @@
<script>
-import { GlFormCheckboxGroup, GlFormCheckbox } from '@gitlab/ui';
+import { GlFormCheckboxGroup, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions } from 'vuex';
+import { s__ } from '~/locale';
import Tracking from '~/tracking';
import { parseBoolean } from '~/lib/utils/common_utils';
@@ -13,6 +14,12 @@ export default {
GlFormCheckboxGroup,
GlFormCheckbox,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ tooltip: s__('GlobalSearch|Include search results from archived projects'),
+ },
computed: {
...mapState(['urlQuery', 'useSidebarNavigation']),
selectedFilter: {
@@ -20,9 +27,9 @@ export default {
return [parseBoolean(this.urlQuery?.include_archived)];
},
set(value) {
- const newValue = value?.pop() ?? false;
- this.setQuery({ key: archivedFilterData.filterParam, value: newValue?.toString() });
- this.trackSelectCheckbox(newValue);
+ const includeArchived = [...value].pop() ?? false;
+ this.setQuery({ key: archivedFilterData.filterParam, value: includeArchived?.toString() });
+ this.trackSelectCheckbox(includeArchived);
},
},
},
@@ -49,7 +56,7 @@ export default {
:class="$options.LABEL_DEFAULT_CLASSES"
:value="true"
>
- <span data-testid="label">
+ <span v-gl-tooltip="$options.i18n.tooltip" data-testid="label">
{{ $options.archivedFilterData.checkboxLabel }}
</span>
</gl-form-checkbox>
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index dbd52978163..4a2d3df6921 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -42,9 +42,8 @@ export default {
},
showArchivedFilter() {
return (
- Object.values(archivedFilterData.scopes).includes(this.currentScope) &&
- this.glFeatures.searchIssuesHideArchivedProjects &&
- this.searchType === SEARCH_TYPE_ADVANCED
+ archivedFilterData.scopes.includes(this.currentScope) &&
+ this.glFeatures.searchIssuesHideArchivedProjects
);
},
showDivider() {
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
index a6af789baad..ebd0406bcec 100644
--- a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
@@ -225,7 +225,7 @@ export default {
v-if="isFocused"
v-outside="closeDropdown"
data-testid="header-search-dropdown-menu"
- class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0 gl-mt-3 gl-z-index-1"
+ class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0 gl-mt-3 gl-z-index-2"
:class="{
'gl-max-w-none!': useSidebarNavigation,
'gl-min-w-full!': useSidebarNavigation,
diff --git a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
index 2845eb2049b..6e476ef7935 100644
--- a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
@@ -2,7 +2,7 @@
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { HR_DEFAULT_CLASSES, SEARCH_TYPE_ADVANCED } from '../constants';
+import { HR_DEFAULT_CLASSES } from '../constants';
import { statusFilterData } from './status_filter/data';
import StatusFilter from './status_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
@@ -22,9 +22,8 @@ export default {
...mapState(['useSidebarNavigation', 'searchType']),
showArchivedFilter() {
return (
- Object.values(archivedFilterData.scopes).includes(this.currentScope) &&
- this.glFeatures.searchMergeRequestsHideArchivedProjects &&
- this.searchType === SEARCH_TYPE_ADVANCED
+ archivedFilterData.scopes.includes(this.currentScope) &&
+ this.glFeatures.searchMergeRequestsHideArchivedProjects
);
},
showStatusFilter() {
diff --git a/app/assets/javascripts/search/sidebar/components/milestones_filters.vue b/app/assets/javascripts/search/sidebar/components/milestones_filters.vue
new file mode 100644
index 00000000000..098e2980c3f
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/milestones_filters.vue
@@ -0,0 +1,18 @@
+<script>
+import ArchivedFilter from './archived_filter/index.vue';
+import FiltersTemplate from './filters_template.vue';
+
+export default {
+ name: 'MilestonesFilters',
+ components: {
+ ArchivedFilter,
+ FiltersTemplate,
+ },
+};
+</script>
+
+<template>
+ <filters-template>
+ <archived-filter class="gl-mb-5" />
+ </filters-template>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index 19df875c292..b5446ecbb42 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -4,6 +4,7 @@ export const SCOPE_BLOB = 'blobs';
export const SCOPE_PROJECTS = 'projects';
export const SCOPE_NOTES = 'notes';
export const SCOPE_COMMITS = 'commits';
+export const SCOPE_MILESTONES = 'milestones';
export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',
'gl-flex-direction-row',
diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js
index f3b4a09b45b..ad47cd975f8 100644
--- a/app/assets/javascripts/search/store/constants.js
+++ b/app/assets/javascripts/search/store/constants.js
@@ -35,3 +35,7 @@ export const ICON_MAP = {
wiki_blobs: 'book',
snippet_titles: 'snippet',
};
+
+export const ZOEKT_SEARCH_TYPE = 'zoekt';
+export const ADVANCED_SEARCH_TYPE = 'advanced';
+export const BASIC_SEARCH_TYPE = 'basic';
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
index ee66bdb2632..49e66492519 100644
--- a/app/assets/javascripts/search/topbar/components/app.vue
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -5,7 +5,8 @@ import { mapState, mapActions } from 'vuex';
import { s__ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
import MarkdownDrawer from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
-import { SYNTAX_OPTIONS_DOCUMENT } from '../constants';
+import { ZOEKT_SEARCH_TYPE, ADVANCED_SEARCH_TYPE } from '~/search/store/constants';
+import { SYNTAX_OPTIONS_ADVANCED_DOCUMENT, SYNTAX_OPTIONS_ZOEKT_DOCUMENT } from '../constants';
import GroupFilter from './group_filter.vue';
import ProjectFilter from './project_filter.vue';
@@ -42,11 +43,6 @@ export default {
required: false,
default: () => ({}),
},
- elasticsearchEnabled: {
- type: Boolean,
- required: false,
- default: false,
- },
defaultBranchName: {
type: String,
required: false,
@@ -54,7 +50,7 @@ export default {
},
},
computed: {
- ...mapState(['query']),
+ ...mapState(['query', 'searchType']),
search: {
get() {
return this.query ? this.query.search : '';
@@ -67,7 +63,15 @@ export default {
return !parseBoolean(this.query.snippets);
},
showSyntaxOptions() {
- return this.elasticsearchEnabled && this.isDefaultBranch;
+ return (
+ (this.searchType === ZOEKT_SEARCH_TYPE || this.searchType === ADVANCED_SEARCH_TYPE) &&
+ this.isDefaultBranch
+ );
+ },
+ documentBasedOnSearchType() {
+ return this.searchType === ZOEKT_SEARCH_TYPE
+ ? SYNTAX_OPTIONS_ZOEKT_DOCUMENT
+ : SYNTAX_OPTIONS_ADVANCED_DOCUMENT;
},
isDefaultBranch() {
return !this.query.repository_ref || this.query.repository_ref === this.defaultBranchName;
@@ -82,7 +86,6 @@ export default {
this.$refs.markdownDrawer.toggleDrawer();
},
},
- SYNTAX_OPTIONS_DOCUMENT,
};
</script>
@@ -104,10 +107,7 @@ export default {
@click="onToggleDrawer"
>{{ $options.i18n.syntaxOptionsLabel }}
</gl-button>
- <markdown-drawer
- ref="markdownDrawer"
- :document-path="$options.SYNTAX_OPTIONS_DOCUMENT"
- />
+ <markdown-drawer ref="markdownDrawer" :document-path="documentBasedOnSearchType" />
</template>
</div>
<gl-search-box-by-click
diff --git a/app/assets/javascripts/search/topbar/constants.js b/app/assets/javascripts/search/topbar/constants.js
index 5b1c5819f2b..1ad40fbe3db 100644
--- a/app/assets/javascripts/search/topbar/constants.js
+++ b/app/assets/javascripts/search/topbar/constants.js
@@ -20,4 +20,5 @@ export const PROJECT_DATA = {
fullName: 'name_with_namespace',
};
-export const SYNTAX_OPTIONS_DOCUMENT = 'drawers/drawers/advanced_search_syntax.md';
+export const SYNTAX_OPTIONS_ADVANCED_DOCUMENT = 'drawers/drawers/advanced_search_syntax.md';
+export const SYNTAX_OPTIONS_ZOEKT_DOCUMENT = 'drawers/drawers/exact_code_search_syntax.md';
diff --git a/app/assets/javascripts/search/topbar/index.js b/app/assets/javascripts/search/topbar/index.js
index d6e16085c28..aad7445ebdc 100644
--- a/app/assets/javascripts/search/topbar/index.js
+++ b/app/assets/javascripts/search/topbar/index.js
@@ -11,18 +11,10 @@ export const initTopbar = (store) => {
return false;
}
- const {
- groupInitialJson,
- projectInitialJson,
- elasticsearchEnabled,
- defaultBranchName,
- } = el.dataset;
+ const { groupInitialJson, projectInitialJson, defaultBranchName } = el.dataset;
const groupInitialJsonParsed = JSON.parse(groupInitialJson);
const projectInitialJsonParsed = JSON.parse(projectInitialJson);
- const elasticsearchEnabledParsed = elasticsearchEnabled
- ? JSON.parse(elasticsearchEnabled)
- : false;
return new Vue({
el,
@@ -32,7 +24,6 @@ export const initTopbar = (store) => {
props: {
groupInitialJson: groupInitialJsonParsed,
projectInitialJson: projectInitialJsonParsed,
- elasticsearchEnabled: elasticsearchEnabledParsed,
defaultBranchName,
},
});
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index 7f0a049a6ad..395bdad5dcc 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -95,6 +95,9 @@ export default {
showSecondaryConfigurationHelpPath() {
return Boolean(this.available && this.feature.secondary?.configurationHelpPath);
},
+ hyphenatedFeature() {
+ return this.feature.type.replace(/_/g, '-');
+ },
},
methods: {
onError(message) {
@@ -167,7 +170,7 @@ export default {
:href="feature.configurationPath"
variant="confirm"
:category="configurationButton.category"
- :data-testid="`${feature.type}_enable_button`"
+ :data-testid="`${hyphenatedFeature}-enable-button`"
class="gl-mt-5"
>
{{ configurationButton.text }}
@@ -179,7 +182,7 @@ export default {
variant="confirm"
:category="manageViaMrButtonCategory"
class="gl-mt-5"
- :data-testid="`${feature.type}_mr_button`"
+ :data-testid="`${hyphenatedFeature}-mr-button`"
@error="onError"
/>
diff --git a/app/assets/javascripts/sentry/init_sentry.js b/app/assets/javascripts/sentry/init_sentry.js
index dbd12dc36ce..6f32c8c4165 100644
--- a/app/assets/javascripts/sentry/init_sentry.js
+++ b/app/assets/javascripts/sentry/init_sentry.js
@@ -4,11 +4,10 @@ import {
defaultStackParser,
makeFetchTransport,
defaultIntegrations,
+ BrowserTracing,
// exports
captureException,
- captureMessage,
- withScope,
SDK_VERSION,
} from 'sentrybrowser';
@@ -19,6 +18,8 @@ const initSentry = () => {
const hub = getCurrentHub();
+ const page = document?.body?.dataset?.page;
+
const client = new BrowserClient({
// Sentry.init(...) options
dsn: gon.sentry_dsn,
@@ -37,7 +38,19 @@ const initSentry = () => {
// https://github.com/getsentry/sentry-javascript/blob/7.66.0/MIGRATION.md#explicit-client-options
transport: makeFetchTransport,
stackParser: defaultStackParser,
- integrations: defaultIntegrations,
+ integrations: [
+ ...defaultIntegrations,
+ new BrowserTracing({
+ beforeNavigate(context) {
+ return {
+ ...context,
+ // `page` acts as transaction name for performance tracing.
+ // If missing, use default Sentry behavior: window.location.pathname
+ name: page || window?.location?.pathname,
+ };
+ },
+ }),
+ ],
});
hub.bindClient(client);
@@ -45,7 +58,7 @@ const initSentry = () => {
hub.setTags({
revision: gon.revision,
feature_category: gon.feature_category,
- page: document?.body?.dataset?.page,
+ page,
});
if (gon.current_user_id) {
@@ -68,8 +81,6 @@ const initSentry = () => {
// eslint-disable-next-line no-underscore-dangle
window._Sentry = {
captureException,
- captureMessage,
- withScope,
SDK_VERSION, // used to verify compatibility with the Sentry instance
};
};
diff --git a/app/assets/javascripts/sentry/sentry_browser_wrapper.js b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
index fbfd5d4f458..03cf53fabef 100644
--- a/app/assets/javascripts/sentry/sentry_browser_wrapper.js
+++ b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
@@ -13,19 +13,3 @@ export const captureException = (...args) => {
Sentry?.captureException(...args);
};
-
-/** @type {import('@sentry/core').captureMessage} */
-export const captureMessage = (...args) => {
- // eslint-disable-next-line no-underscore-dangle
- const Sentry = window._Sentry;
-
- Sentry?.captureMessage(...args);
-};
-
-/** @type {import('@sentry/core').withScope} */
-export const withScope = (...args) => {
- // eslint-disable-next-line no-underscore-dangle
- const Sentry = window._Sentry;
-
- Sentry?.withScope(...args);
-};
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index da948cc85b6..1eee7a932a4 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -52,7 +52,7 @@ export function initTrackProductAnalyticsExpanded() {
const $analyticsSection = $('#js-product-analytics-settings');
$analyticsSection.on('click.toggleSection', '.js-settings-toggle', () => {
if (isExpanded($analyticsSection)) {
- InternalEvents.track_event('user_viewed_cluster_configuration');
+ InternalEvents.trackEvent('user_viewed_cluster_configuration');
}
});
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
index d65c950b33a..81fc2267622 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
@@ -50,7 +50,7 @@ export default {
:width="imgSize"
:class="`s${imgSize}`"
class="avatar avatar-inline m-0"
- data-qa-selector="avatar_image"
+ data-testid="avatar-image"
/>
<gl-icon v-if="hasMergeIcon" name="warning-solid" aria-hidden="true" class="merge-icon" />
</span>
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 ef7f12f273f..a4090800ae6 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -92,7 +92,6 @@ export default {
<div
class="gl-ml-3 gl-line-height-normal gl-display-grid gl-align-items-center"
data-testid="username"
- data-qa-selector="username"
>
<user-name-with-status :name="user.name" :availability="userAvailability(user)" />
</div>
@@ -104,7 +103,6 @@ export default {
category="tertiary"
size="small"
data-testid="user-list-more-button"
- data-qa-selector="more_assignees_link"
@click="toggleShowLess"
>
<template v-if="showLess">
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
index 7a1853b1b46..90c3fb0039d 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import { TYPE_ISSUE, TYPE_TEST_CASE, IssuableTypeText } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_TEST_CASE, issuableTypeText } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import { confidentialityQueries } from '../../queries/constants';
@@ -80,7 +80,7 @@ export default {
: __('at least the Reporter role');
},
issuableTypeText() {
- return IssuableTypeText[this.issuableType];
+ return issuableTypeText[this.issuableType];
},
commentText() {
return this.isTestCase ? '' : __(' and leave a comment on');
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
index 295d37671cc..ecccb0abfd1 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
@@ -135,6 +135,7 @@ export default {
:tracking="$options.tracking"
:loading="isLoading"
class="block confidentiality"
+ data-testid="sidebar-confidentiality"
>
<template #collapsed>
<div>
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
index 3e4297887f0..a1b7e65474a 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
@@ -183,7 +183,7 @@ export default {
ref="searchInput"
v-model="searchKey"
:disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
+ data-testid="dropdown-input-field"
/>
</div>
<div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
index 154a8e866d0..377200ab804 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
@@ -71,8 +71,7 @@ export default {
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
- data-testid="close-button"
- data-qa-selector="close_labels_dropdown_button"
+ data-testid="close-labels-dropdown-button"
@click="$emit('closeDropdown')"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
index 57e3ee4aaa5..f2ce02526e7 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
@@ -108,7 +108,7 @@ export default {
v-for="label in sortedSelectedLabels"
:key="label.id"
class="hide-collapsed"
- data-qa-selector="selected_label_content"
+ data-testid="selected-label-content"
:data-qa-label-name="label.title"
:title="label.title"
:description="label.description"
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
index f9a9cc316c1..ac52e4dbf3f 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
@@ -362,7 +362,6 @@ export default {
'is-embedded': isDropdownVariantEmbedded(variant),
}"
data-testid="sidebar-labels"
- data-qa-selector="labels_block"
>
<template v-if="isDropdownVariantSidebar(variant)">
<sidebar-editable-item
diff --git a/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue
index 24afb25e403..f2097ce589e 100644
--- a/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue
@@ -110,7 +110,7 @@ export default {
:issuable-attribute="$options.issuableAttribute"
:issuable-type="issuableType"
:workspace-type="workspaceType"
- data-qa-selector="issuable_milestone_dropdown"
+ data-testid="issuable-milestone-dropdown"
@change="handleChange"
>
<template #footer>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
index 55bb214aa65..92461183711 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -43,7 +43,7 @@ export default {
data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="reviewer"
- data-qa-selector="reviewers_edit_button"
+ data-testid="reviewers-edit-button"
>
{{ __('Edit') }}
</a>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index a3282932f84..ee9edd6a022 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -1,5 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
+import { GlButton } from '@gitlab/ui';
// 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 { TYPE_ISSUE } from '~/issues/constants';
@@ -11,6 +12,7 @@ export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Reviewers',
components: {
+ GlButton,
CollapsedReviewerList,
UncollapsedReviewerList,
},
@@ -64,15 +66,16 @@ export default {
{{ __('None') }}
<template v-if="editable">
-
- <button
- type="button"
- class="gl-button btn-link gl-reset-color!"
+ <gl-button
+ category="tertiary"
+ variant="link"
+ class="gl-ml-2"
data-testid="assign-yourself"
data-qa-selector="assign_yourself_button"
@click="assignSelf"
>
- {{ __('assign yourself') }}
- </button>
+ <span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
+ </gl-button>
</template>
</span>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 7fde43a360d..28b88a59405 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -308,7 +308,7 @@ export default {
v-gl-tooltip="tooltipText"
class="gl-reset-color gl-hover-text-blue-800"
:href="attributeUrl"
- :data-qa-selector="`${formatIssuableAttribute.snake}_link`"
+ :data-testid="`${formatIssuableAttribute.kebab}-link`"
>
{{ attributeTitle }}
<span v-if="isAttributeOverdue(currentAttribute)">{{ $options.i18n.expired }}</span>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index 568962cddc7..866db2a43b8 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -1,7 +1,7 @@
<script>
import {
+ GlButton,
GlDisclosureDropdownItem,
- GlDropdownForm,
GlIcon,
GlLoadingIcon,
GlToggle,
@@ -30,8 +30,8 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
+ GlButton,
GlDisclosureDropdownItem,
- GlDropdownForm,
GlIcon,
GlLoadingIcon,
GlToggle,
@@ -130,6 +130,12 @@ export default {
canSubscribe() {
return this.emailsDisabled || !this.isLoggedIn;
},
+ isNotificationsTodosButtons() {
+ return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
+ },
+ isMergeRequest() {
+ return this.issuableType === 'merge_request';
+ },
},
methods: {
setSubscribed(subscribed) {
@@ -194,20 +200,8 @@ export default {
</script>
<template>
- <gl-dropdown-form v-if="isMovedMrSidebar && isIssuable" class="gl-dropdown-item">
- <div class="gl-px-5 gl-pb-2 gl-pt-1">
- <gl-toggle
- :value="subscribed"
- :label="$options.i18n.notifications"
- class="merge-request-notification-toggle"
- label-position="left"
- data-testid="notification-toggle"
- @change="toggleSubscribed"
- />
- </div>
- </gl-dropdown-form>
<gl-disclosure-dropdown-item
- v-else-if="isMovedMrSidebar"
+ v-if="isMovedMrSidebar && !isNotificationsTodosButtons"
data-testid="notification-toggle"
@action="toggleSubscribed"
>
@@ -220,6 +214,32 @@ export default {
/>
</template>
</gl-disclosure-dropdown-item>
+ <div v-else-if="isNotificationsTodosButtons" :class="{ 'inline-block': !isMergeRequest }">
+ <gl-button
+ ref="tooltip"
+ v-gl-tooltip.hover.top
+ category="secondary"
+ data-testid="subscribe-button"
+ class="hide-collapsed"
+ :title="notificationTooltip"
+ :class="{ 'gl-ml-2': isIssuable, 'btn-icon': isNotificationsTodosButtons }"
+ @click="toggleSubscribed"
+ >
+ <gl-icon :name="notificationIcon" :size="16" :class="{ 'gl-fill-blue-500': subscribed }" />
+ </gl-button>
+ <gl-button
+ v-if="!isMergeRequest"
+ ref="tooltip"
+ v-gl-tooltip.left.viewport
+ category="secondary"
+ data-testid="subscribe-button"
+ :title="notificationTooltip"
+ class="sidebar-collapsed-icon sidebar-collapsed-container gl-rounded-0! gl-shadow-none!"
+ @click="toggleSubscribed"
+ >
+ <gl-icon :name="notificationIcon" :size="16" :class="{ 'gl-fill-blue-500': subscribed }" />
+ </gl-button>
+ </div>
<sidebar-editable-item
v-else
ref="editable"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue b/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue
index 9b582ba41ed..f11c7e6ac4d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue
@@ -206,7 +206,7 @@ export default {
:value="spentAt"
show-clear-button
autocomplete="off"
- size="small"
+ width="small"
@input="updateSpentAtDate"
@clear="updateSpentAtDate(null)"
/>
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
index 1099dcb832f..f2257adb79c 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
@@ -114,6 +114,9 @@ export default {
tootltipTitle() {
return todoLabel(this.hasTodo);
},
+ isNotificationsTodosButtons() {
+ return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
+ },
},
methods: {
toggleTodo() {
@@ -183,8 +186,26 @@ export default {
</script>
<template>
- <div data-testid="sidebar-todo">
+ <div data-testid="sidebar-todo" :class="{ 'inline-block': !isMergeRequest }">
+ <todo-button
+ v-if="isNotificationsTodosButtons"
+ v-gl-tooltip.hover.top
+ :title="tootltipTitle"
+ :issuable-type="issuableType"
+ :issuable-id="issuableId"
+ :is-todo="hasTodo"
+ :disabled="isLoading"
+ class="hide-collapsed btn-icon"
+ @click.stop.prevent="toggleTodo"
+ >
+ <gl-icon
+ v-if="isNotificationsTodosButtons"
+ :class="{ 'todo-undone gl-fill-blue-500': hasTodo }"
+ :name="collapsedButtonIcon"
+ />
+ </todo-button>
<todo-button
+ v-else
:issuable-type="issuableType"
:issuable-id="issuableId"
:is-todo="hasTodo"
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue
index b49b8fc389b..2aa79b45093 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue
@@ -39,6 +39,6 @@ export default {
<template>
<gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="onToggle($event)">
- {{ buttonLabel }}
+ <slot>{{ buttonLabel }}</slot>
</gl-button>
</template>
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 1f3119e14db..4b6dbdcc2c9 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -15,7 +15,6 @@ import { __ } from '~/locale';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import Translate from '~/vue_shared/translate';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
-import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue';
@@ -800,21 +799,6 @@ export function mountAssigneesDropdown() {
});
}
-function mountNewIssuePopover() {
- const el = document.querySelector('.js-sidebar-header-popover');
-
- if (!el) {
- return null;
- }
-
- return new Vue({
- el,
- name: 'NewHeaderActionsPopover',
- render: (createElement) =>
- createElement(NewHeaderActionsPopover, { props: { issueType: TYPE_MERGE_REQUEST } }),
- });
-}
-
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
@@ -840,7 +824,6 @@ export function mountSidebar(mediator, store) {
mountSidebarSeverityWidget();
mountSidebarEscalationStatus();
mountMoveIssueButton();
- mountNewIssuePopover();
}
export { getSidebarOptions };
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 9e80210de51..aa3f33989c8 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -232,8 +232,7 @@ export default {
<gl-form-input
id="snippet-title"
v-model="snippet.title"
- data-testid="snippet-title-input"
- data-qa-selector="snippet_title_field"
+ data-testid="snippet-title-input-field"
:autofocus="true"
/>
</gl-form-group>
@@ -261,7 +260,7 @@ export default {
category="primary"
type="submit"
variant="confirm"
- data-qa-selector="submit_button"
+ data-testid="submit-button"
:disabled="isUpdating"
>{{ saveButtonLabel }}</gl-button
>
diff --git a/app/assets/javascripts/snippets/components/embed_dropdown.vue b/app/assets/javascripts/snippets/components/embed_dropdown.vue
index 17312c2373b..1510cc01810 100644
--- a/app/assets/javascripts/snippets/components/embed_dropdown.vue
+++ b/app/assets/javascripts/snippets/components/embed_dropdown.vue
@@ -53,7 +53,7 @@ export default {
:aria-label="$options.MSG_COPY"
:data-clipboard-text="value"
icon="copy-to-clipboard"
- data-qa-selector="copy_button"
+ data-testid="copy-button"
:data-qa-action="name"
/>
</template>
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index 549b1bdd209..7a60fc6d26c 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -68,14 +68,14 @@ export default {
<embed-dropdown
v-if="embeddable"
:url="snippet.webUrl"
- data-qa-selector="snippet_embed_dropdown"
+ data-testid="snippet-embed-dropdown"
/>
<clone-dropdown-button
v-if="canBeCloned"
class="gl-ml-3"
:ssh-link="snippet.sshUrlToRepo"
:http-link="snippet.httpUrlToRepo"
- data-qa-selector="clone_button"
+ data-testid="clone-button"
/>
</div>
<gl-alert v-if="hasUnretrievableBlobs" variant="danger" class="gl-mb-3" :dismissible="false">
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 59f7c8d8d97..ca1d9f858a5 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
@@ -157,10 +157,9 @@ export default {
</gl-form-group>
<gl-button
:disabled="!canAdd"
- data-testid="add_button"
+ data-testid="add-button"
class="gl-my-3"
variant="dashed"
- data-qa-selector="add_file_button"
@click="addBlob"
>{{ addLabel }}</gl-button
>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index 021bd23781e..9b0a1db23f2 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -69,11 +69,11 @@ export default {
};
</script>
<template>
- <div class="file-holder snippet" data-qa-selector="file_holder_container">
+ <div class="file-holder snippet" data-testid="file-holder-container">
<blob-header-edit
:id="inputId"
:value="blob.path"
- data-qa-selector="file_name_field"
+ data-testid="file-name-field"
:can-delete="canDelete"
:show-delete="showDelete"
@input="notifyAboutUpdates({ path: $event })"
diff --git a/app/assets/javascripts/snippets/components/snippet_description_edit.vue b/app/assets/javascripts/snippets/components/snippet_description_edit.vue
index 3ce7ea231ff..93d52890675 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_edit.vue
@@ -36,7 +36,7 @@ export default {
<gl-form-input
class="form-control"
:placeholder="s__('Snippets|Describe what your snippet does or how to use it…')"
- data-qa-selector="description_placeholder"
+ data-testid="description-placeholder"
/>
</div>
<markdown-field
@@ -54,7 +54,7 @@ export default {
:value="value"
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
- data-qa-selector="snippet_description_field"
+ data-testid="snippet-description-field"
data-supports-quick-actions="false"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
diff --git a/app/assets/javascripts/snippets/components/snippet_description_view.vue b/app/assets/javascripts/snippets/components/snippet_description_view.vue
index ab2ff6e0ef8..9eae096d6f2 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_view.vue
@@ -20,7 +20,7 @@ export default {
};
</script>
<template>
- <markdown-field-view class="snippet-description" data-qa-selector="snippet_description_content">
+ <markdown-field-view class="snippet-description" data-testid="snippet-description-content">
<div
v-safe-html:[$options.safeHtmlConfig]="description"
class="md js-snippet-description"
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 881e06113d9..56ea931fc8c 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -216,7 +216,7 @@ export default {
<div class="detail-page-header-body">
<div
class="snippet-box has-tooltip d-flex align-items-center gl-mr-2 mb-1"
- data-qa-selector="snippet_container"
+ data-testid="snippet-container"
:title="snippetVisibilityLevelDescription"
data-container="body"
>
@@ -267,7 +267,7 @@ export default {
:category="action.category"
:class="action.cssClass"
:href="action.href"
- data-qa-selector="snippet_action_button"
+ data-testid="snippet-action-button"
:data-qa-action="action.text"
@click="action.click ? action.click() : undefined"
>{{ action.text }}</gl-button
@@ -321,8 +321,7 @@ export default {
variant="danger"
category="primary"
:disabled="isLoading"
- data-qa-selector="delete_snippet_button"
- data-testid="delete-snippet"
+ data-testid="delete-snippet-button"
@click="deleteSnippet"
>
<gl-loading-icon v-if="isLoading" size="sm" inline />
diff --git a/app/assets/javascripts/snippets/components/snippet_title.vue b/app/assets/javascripts/snippets/components/snippet_title.vue
index 2cf7a1e267b..0e4dbf55963 100644
--- a/app/assets/javascripts/snippets/components/snippet_title.vue
+++ b/app/assets/javascripts/snippets/components/snippet_title.vue
@@ -20,7 +20,7 @@ export default {
</script>
<template>
<div class="snippet-header limited-header-width">
- <h2 class="snippet-title gl-mt-0 mb-3" data-qa-selector="snippet_title_content">
+ <h2 class="snippet-title gl-mt-0 mb-3" data-testid="snippet-title-content">
{{ snippet.title }}
</h2>
diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
index 24dd978585c..37d10cffc78 100644
--- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue
@@ -57,7 +57,7 @@ export default {
<gl-icon :size="16" :name="option.icon" />
<span
class="font-weight-bold ml-1 js-visibility-option"
- data-qa-selector="visibility_content"
+ data-testid="visibility-content"
:data-qa-visibility="option.label"
>{{ option.label }}</span
>
diff --git a/app/assets/javascripts/super_sidebar/components/brand_logo.vue b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
index 02cf36fb053..c280c03591b 100644
--- a/app/assets/javascripts/super_sidebar/components/brand_logo.vue
+++ b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
@@ -26,7 +26,7 @@ export default {
<template>
<a
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.homepage"
+ v-gl-tooltip:super-sidebar.bottom="$options.i18n.homepage"
class="brand-logo"
:href="rootPath"
data-track-action="click_link"
@@ -46,7 +46,7 @@ export default {
<span
v-else
v-safe-html="$options.logo"
- aria-hidden
+ aria-hidden="true"
data-testid="brand-header-default-logo"
></span>
</a>
diff --git a/app/assets/javascripts/super_sidebar/components/counter.vue b/app/assets/javascripts/super_sidebar/components/counter.vue
index c0e1959fba4..49efc5ab5b9 100644
--- a/app/assets/javascripts/super_sidebar/components/counter.vue
+++ b/app/assets/javascripts/super_sidebar/components/counter.vue
@@ -15,7 +15,7 @@ export default {
href: {
type: String,
required: false,
- default: '',
+ default: null,
},
icon: {
type: String,
diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue
index d1e96479631..279e689bd8d 100644
--- a/app/assets/javascripts/super_sidebar/components/create_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue
@@ -14,7 +14,7 @@ import {
import { DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants';
// Left offset required for the dropdown to be aligned with the super sidebar
-const DROPDOWN_X_OFFSET_BASE = -179;
+const DROPDOWN_X_OFFSET_BASE = -177;
const DROPDOWN_X_OFFSET_IMPERSONATING = DROPDOWN_X_OFFSET_BASE + IMPERSONATING_OFFSET;
export default {
@@ -62,7 +62,7 @@ export default {
<template>
<gl-disclosure-dropdown
- v-gl-tooltip:super-sidebar.hover.bottom="dropdownOpen ? '' : $options.i18n.createNew"
+ v-gl-tooltip:super-sidebar.bottom="dropdownOpen ? '' : $options.i18n.createNew"
category="tertiary"
icon="plus"
no-caret
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
index 4cfc329f8b8..61fa360c41f 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
@@ -17,6 +17,7 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { sprintf } from '~/locale';
import { ARROW_DOWN_KEY, ARROW_UP_KEY, END_KEY, HOME_KEY, ESC_KEY } from '~/lib/utils/keys';
import {
+ COMMAND_PALETTE,
MIN_SEARCH_TERM,
SEARCH_DESCRIBED_BY_WITH_RESULTS,
SEARCH_DESCRIBED_BY_DEFAULT,
@@ -50,6 +51,7 @@ export default {
name: 'GlobalSearchModal',
SEARCH_MODAL_ID,
i18n: {
+ COMMAND_PALETTE,
SEARCH_DESCRIBED_BY_WITH_RESULTS,
SEARCH_DESCRIBED_BY_DEFAULT,
SEARCH_DESCRIBED_BY_UPDATED,
@@ -279,6 +281,7 @@ export default {
hide-footer
hide-header-close
scrollable
+ :title="$options.i18n.COMMAND_PALETTE"
body-class="gl-p-0!"
modal-class="global-search-modal"
:centered="false"
diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue
index 8ce82116194..069987d4006 100644
--- a/app/assets/javascripts/super_sidebar/components/help_center.vue
+++ b/app/assets/javascripts/super_sidebar/components/help_center.vue
@@ -38,7 +38,7 @@ export default {
shortcuts: __('Keyboard shortcuts'),
version: __('Your GitLab version'),
whatsnew: __("What's new"),
- chat: s__('TanukiBot|Ask GitLab Duo'),
+ chat: s__('TanukiBot|GitLab Duo Chat'),
},
props: {
sidebarData: {
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index 6b5002e1aa8..91b781b8235 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -152,20 +152,20 @@ export default {
<gl-collapse
:id="itemId"
v-model="isExpanded"
- :aria-label="item.title"
class="gl-list-style-none gl-p-0 gl-m-0 gl-transition-duration-medium gl-transition-timing-function-ease"
data-qa-selector="menu_section"
:data-qa-section-name="item.title"
- tag="ul"
>
<slot>
- <nav-item
- v-for="subItem of item.items"
- :key="`${item.title}-${subItem.title}`"
- :item="subItem"
- @pin-add="(itemId) => $emit('pin-add', itemId)"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
- />
+ <ul :aria-label="item.title" class="gl-list-style-none gl-p-0 gl-m-0">
+ <nav-item
+ v-for="subItem of item.items"
+ :key="`${item.title}-${subItem.title}`"
+ :item="subItem"
+ @pin-add="(itemId) => $emit('pin-add', itemId)"
+ @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ />
+ </ul>
</slot>
</gl-collapse>
</component>
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index 5e0f8fffb0e..5416f86abeb 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -7,6 +7,7 @@ import {
TRACKING_UNKNOWN_ID,
TRACKING_UNKNOWN_PANEL,
} from '~/super_sidebar/constants';
+import eventHub from '../event_hub';
import NavItemLink from './nav_item_link.vue';
import NavItemRouterLink from './nav_item_router_link.vue';
@@ -69,16 +70,14 @@ export default {
return {
isMouseIn: false,
canClickPinButton: false,
+ pillCount: this.item.pill_count,
};
},
computed: {
- pillData() {
- return this.item.pill_count;
- },
hasPill() {
return (
- Number.isFinite(this.pillData) ||
- (typeof this.pillData === 'string' && this.pillData !== '')
+ Number.isFinite(this.pillCount) ||
+ (typeof this.pillCount === 'string' && this.pillCount !== '')
);
},
isPinnable() {
@@ -145,6 +144,9 @@ export default {
hasAvatar() {
return Boolean(this.item.entity_id);
},
+ hasEndSpace() {
+ return this.hasPill || this.isPinnable || this.isFlyout;
+ },
avatarShape() {
return this.item.avatar_shape || 'rect';
},
@@ -179,11 +181,21 @@ export default {
if (this.item.is_active) {
this.$el.scrollIntoView(false);
}
+
+ eventHub.$on('updatePillValue', this.updatePillValue);
+ },
+ destroyed() {
+ eventHub.$off('updatePillValue', this.updatePillValue);
},
methods: {
togglePointerEvents() {
this.canClickPinButton = this.isMouseIn;
},
+ updatePillValue({ value, itemId }) {
+ if (this.item.id === itemId) {
+ this.pillCount = value;
+ }
+ },
},
};
</script>
@@ -236,7 +248,7 @@ export default {
</div>
</div>
<slot name="actions"></slot>
- <span v-if="hasPill || isPinnable" class="gl-text-right gl-relative gl-min-w-8">
+ <span v-if="hasEndSpace" class="gl-text-right gl-relative gl-min-w-6">
<gl-badge
v-if="hasPill"
size="sm"
@@ -246,7 +258,7 @@ export default {
'hide-on-focus-or-hover--target transition-opacity-on-hover--target': isPinnable,
}"
>
- {{ pillData }}
+ {{ pillCount }}
</gl-badge>
</span>
</component>
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index 5da45b52bf4..ea3e9e9df1f 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -102,7 +102,7 @@ export default {
<draggable
v-if="items.length > 0"
v-model="draggableItems"
- class="gl-p-0 gl-m-0"
+ class="gl-p-0 gl-m-0 gl-list-style-none"
data-testid="pinned-nav-items"
handle=".js-draggable-icon"
tag="ul"
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
index 02488e99c0e..772072c0996 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
@@ -158,7 +158,11 @@ export default {
<template>
<div class="gl-p-2 gl-relative">
- <ul v-if="hasStaticItems" class="gl-p-0 gl-m-0" data-testid="static-items-section">
+ <ul
+ v-if="hasStaticItems"
+ class="gl-list-style-none gl-p-0 gl-m-0"
+ data-testid="static-items-section"
+ >
<nav-item v-for="item in staticItems" :key="item.id" :item="item" is-static />
</ul>
<pinned-section
@@ -174,7 +178,11 @@ export default {
class="gl-my-2 gl-mx-4"
data-testid="main-menu-separator"
/>
- <ul class="gl-p-0 gl-list-style-none" data-testid="non-static-items-section">
+ <ul
+ aria-labelledby="super-sidebar-context-header"
+ class="gl-p-0 gl-list-style-none"
+ data-testid="non-static-items-section"
+ >
<template v-for="item in nonStaticItems">
<menu-section
v-if="isSection(item)"
@@ -182,6 +190,7 @@ export default {
:item="item"
:separated="item.separated"
:has-flyout="showFlyoutMenus"
+ tag="li"
@pin-add="createPin"
@pin-remove="destroyPin"
/>
@@ -189,7 +198,6 @@ export default {
v-else
:key="item.id"
:item="item"
- tag="li"
@pin-add="createPin"
@pin-remove="destroyPin"
/>
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
index fe3e4a8199e..5f7cfce93b1 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -36,7 +36,7 @@ export default {
mixins: [Tracking.mixin()],
i18n: {
skipToMainContent: __('Skip to main content'),
- primary: s__('Navigation|Primary'),
+ primaryNavigation: s__('Navigation|Primary navigation'),
},
inject: ['showTrialStatusWidget'],
props: {
@@ -130,7 +130,9 @@ export default {
<div>
<div class="super-sidebar-overlay" @click="collapseSidebar"></div>
<gl-button
+ v-if="sidebarData.is_logged_in"
class="super-sidebar-skip-to gl-sr-only-focusable gl-fixed gl-left-0 gl-m-3"
+ data-testid="super-sidebar-skip-to"
href="#content-body"
variant="confirm"
>
@@ -138,7 +140,7 @@ export default {
</gl-button>
<nav
id="super-sidebar"
- :aria-label="$options.i18n.primary"
+ aria-labelledby="super-sidebar-heading"
class="super-sidebar"
:class="peekClasses"
data-testid="super-sidebar"
@@ -147,6 +149,9 @@ export default {
@mouseenter="isMouseover = true"
@mouseleave="isMouseover = false"
>
+ <h2 id="super-sidebar-heading" class="gl-sr-only">
+ {{ $options.i18n.primaryNavigation }}
+ </h2>
<user-bar :has-collapse-button="!showOverlay" :sidebar-data="sidebarData" />
<div v-if="showTrialStatusWidget" class="gl-px-2 gl-py-2">
<trial-status-widget
@@ -158,12 +163,12 @@ export default {
class="contextual-nav gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden"
>
<div class="gl-flex-grow-1 gl-overflow-auto" data-testid="nav-container">
- <h2
- class="gl-px-5 gl-pt-3 gl-pb-2 gl-m-0 gl-reset-line-height gl-font-sm super-sidebar-context-header"
+ <div
+ id="super-sidebar-context-header"
+ class="gl-px-5 gl-pt-3 gl-pb-2 gl-m-0 gl-reset-line-height gl-font-weight-bold gl-font-sm super-sidebar-context-header"
>
{{ sidebarData.current_context_header }}
- </h2>
-
+ </div>
<sidebar-menu
v-if="menuItems.length"
:items="menuItems"
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
index 30ee18cc369..71c1460423e 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
@@ -14,66 +14,82 @@ export default {
},
mixins: [Tracking.mixin()],
props: {
- tooltipContainer: {
+ type: {
type: String,
required: false,
- default: null,
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'right',
+ default: 'expand',
},
},
i18n: {
- collapseSidebar: __('Hide sidebar'),
- expandSidebar: __('Keep sidebar visible'),
primaryNavigationSidebar: __('Primary navigation sidebar'),
},
+ tooltipCollapse: {
+ placement: 'bottom',
+ container: 'super-sidebar',
+ title: __('Hide sidebar'),
+ },
+ tooltipExpand: {
+ placement: 'right',
+ title: __('Keep sidebar visible'),
+ },
data() {
return sidebarState;
},
computed: {
- canOpen() {
- return this.isCollapsed || this.isPeek || this.isHoverPeek;
+ isTypeCollapse() {
+ return this.type === 'collapse';
},
- tooltipTitle() {
- return this.canOpen ? this.$options.i18n.expandSidebar : this.$options.i18n.collapseSidebar;
+ isTypeExpand() {
+ return this.type === 'expand';
},
tooltip() {
- return {
- placement: this.tooltipPlacement,
- container: this.tooltipContainer,
- title: this.tooltipTitle,
- };
+ return this.isTypeExpand ? this.$options.tooltipExpand : this.$options.tooltipCollapse;
},
ariaExpanded() {
- return String(!this.canOpen);
+ return String(this.isTypeCollapse);
},
},
+ mounted() {
+ this.$root.$on('bv::tooltip::show', this.onTooltipShow);
+ },
+ beforeUnmount() {
+ this.$root.$off('bv::tooltip::show', this.onTooltipShow);
+ },
methods: {
toggle() {
- this.track(this.canOpen ? 'nav_show' : 'nav_hide', {
+ this.track(this.isTypeExpand ? 'nav_show' : 'nav_hide', {
label: 'nav_toggle',
property: 'nav_sidebar',
});
- toggleSuperSidebarCollapsed(!this.canOpen, true);
+ toggleSuperSidebarCollapsed(!this.isTypeExpand, true);
this.focusOtherToggle();
},
focusOtherToggle() {
this.$nextTick(() => {
- const classSelector = this.canOpen ? JS_TOGGLE_EXPAND_CLASS : JS_TOGGLE_COLLAPSE_CLASS;
+ const classSelector = this.isTypeExpand ? JS_TOGGLE_COLLAPSE_CLASS : JS_TOGGLE_EXPAND_CLASS;
const otherToggle = document.querySelector(`.${classSelector}`);
otherToggle?.focus();
});
},
+ onTooltipShow(bvEvent) {
+ if (
+ bvEvent.target !== this.$el ||
+ (this.isTypeCollapse && !this.isCollapsed) ||
+ (this.isTypeExpand && this.isCollapsed) ||
+ this.isPeek ||
+ this.isHoverPeek
+ )
+ return;
+
+ bvEvent.preventDefault();
+ },
},
};
</script>
<template>
<gl-button
- v-gl-tooltip.hover="tooltip"
+ v-gl-tooltip="tooltip"
aria-controls="super-sidebar"
:aria-expanded="ariaExpanded"
:aria-label="$options.i18n.primaryNavigationSidebar"
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 49aee4f3470..88ea4d828b7 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -126,9 +126,8 @@ export default {
<super-sidebar-toggle
v-if="hasCollapseButton"
:class="$options.JS_TOGGLE_COLLAPSE_CLASS"
- tooltip-placement="bottom"
- tooltip-container="super-sidebar"
data-testid="super-sidebar-collapse-button"
+ type="collapse"
/>
<create-menu
v-if="sidebarData.is_logged_in && sidebarData.create_new_menu_groups.length > 0"
@@ -154,7 +153,7 @@ export default {
class="gl-display-flex gl-justify-content-space-between gl-gap-2"
>
<counter
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.issues"
+ v-gl-tooltip:super-sidebar.bottom="$options.i18n.issues"
class="gl-flex-basis-third dashboard-shortcuts-issues"
icon="issues"
:count="userCounts.assigned_issues"
@@ -172,7 +171,7 @@ export default {
@hidden="mrMenuShown = false"
>
<counter
- v-gl-tooltip:super-sidebar.hover.bottom="mrMenuShown ? '' : $options.i18n.mergeRequests"
+ v-gl-tooltip:super-sidebar.bottom="mrMenuShown ? '' : $options.i18n.mergeRequests"
class="gl-w-full"
icon="merge-request-open"
:count="mergeRequestTotalCount"
@@ -184,7 +183,7 @@ export default {
/>
</merge-request-menu>
<counter
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.todoList"
+ v-gl-tooltip:super-sidebar.bottom="$options.i18n.todoList"
class="gl-flex-basis-third shortcuts-todos js-todos-count"
icon="todo-done"
:count="userCounts.todos"
@@ -198,7 +197,7 @@ export default {
</div>
<button
id="super-sidebar-search"
- v-gl-tooltip.bottom.hover.html="searchTooltip"
+ v-gl-tooltip.bottom.html="searchTooltip"
v-gl-modal="$options.SEARCH_MODAL_ID"
class="counter gl-display-block gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-inset-border-1-gray-a-08 gl-line-height-1 gl-focus--focus gl-w-full"
data-testid="super-sidebar-search-button"
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index ed6c41e85c6..891e883b6c0 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -12,7 +12,7 @@ import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import Tracking from '~/tracking';
import PersistentUserCallout from '~/persistent_user_callout';
import { USER_MENU_TRACKING_DEFAULTS, DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants';
-import UserNameGroup from './user_name_group.vue';
+import UserMenuProfileItem from './user_menu_profile_item.vue';
// Left offset required for the dropdown to be aligned with the super sidebar
const DROPDOWN_X_OFFSET_BASE = -211;
@@ -40,7 +40,7 @@ export default {
GlDisclosureDropdownItem,
GlButton,
NewNavToggle,
- UserNameGroup,
+ UserMenuProfileItem,
},
directives: {
SafeHtml,
@@ -247,7 +247,10 @@ export default {
</gl-button>
</template>
- <user-name-group :user="data" />
+ <gl-disclosure-dropdown-group>
+ <user-menu-profile-item :user="data" />
+ </gl-disclosure-dropdown-group>
+
<gl-disclosure-dropdown-group bordered>
<gl-disclosure-dropdown-item
v-if="data.status.can_update"
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue b/app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue
new file mode 100644
index 00000000000..95255ce3d8e
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlBadge, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { s__ } from '~/locale';
+import { USER_MENU_TRACKING_DEFAULTS } from '../constants';
+
+export default {
+ i18n: {
+ user: {
+ busy: s__('UserProfile|Busy'),
+ },
+ },
+ components: {
+ GlBadge,
+ GlDisclosureDropdownItem,
+ GlTooltip,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ user: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ menuItem() {
+ const item = {
+ text: this.user.name,
+ };
+ if (this.user.has_link_to_profile) {
+ item.href = this.user.link_to_profile;
+
+ item.extraAttrs = {
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'user_profile',
+ 'data-testid': 'user-profile-link',
+ };
+ }
+
+ return item;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-item :item="menuItem">
+ <template #list-item>
+ <span class="gl-display-flex gl-flex-direction-column">
+ <span>
+ <span class="gl-font-weight-bold">
+ {{ user.name }}
+ </span>
+ <gl-badge v-if="user.status.busy" size="sm" variant="warning">
+ {{ $options.i18n.user.busy }}
+ </gl-badge>
+ </span>
+
+ <span class="gl-text-gray-400 gl-word-break-all">@{{ user.username }}</span>
+
+ <span
+ v-if="user.status.customized"
+ ref="statusTooltipTarget"
+ data-testid="user-menu-status"
+ class="gl-display-flex gl-align-items-baseline gl-mt-2 gl-font-sm"
+ >
+ <gl-emoji :data-name="user.status.emoji" class="gl-mr-1" />
+ <span v-safe-html="user.status.message_html" class="gl-text-truncate"></span>
+ <gl-tooltip
+ v-if="user.status.message_html"
+ :target="() => $refs.statusTooltipTarget"
+ boundary="viewport"
+ placement="bottom"
+ >
+ <span v-safe-html="user.status.message_html"></span>
+ </gl-tooltip>
+ </span>
+ </span>
+ </template>
+ </gl-disclosure-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_name_group.vue b/app/assets/javascripts/super_sidebar/components/user_name_group.vue
deleted file mode 100644
index 3c8059387fa..00000000000
--- a/app/assets/javascripts/super_sidebar/components/user_name_group.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<script>
-import {
- GlBadge,
- GlDisclosureDropdownGroup,
- GlDisclosureDropdownItem,
- GlTooltip,
-} from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { s__ } from '~/locale';
-import { USER_MENU_TRACKING_DEFAULTS } from '../constants';
-
-export default {
- i18n: {
- user: {
- busy: s__('UserProfile|Busy'),
- },
- },
- components: {
- GlBadge,
- GlDisclosureDropdownGroup,
- GlDisclosureDropdownItem,
- GlTooltip,
- },
- directives: {
- SafeHtml,
- },
- props: {
- user: {
- required: true,
- type: Object,
- },
- },
- computed: {
- menuItem() {
- const item = {
- text: this.user.name,
- };
- if (this.user.has_link_to_profile) {
- item.href = this.user.link_to_profile;
-
- item.extraAttrs = {
- ...USER_MENU_TRACKING_DEFAULTS,
- 'data-track-label': 'user_profile',
- 'data-testid': 'user_profile_link',
- };
- }
-
- return item;
- },
- },
-};
-</script>
-
-<template>
- <gl-disclosure-dropdown-group>
- <gl-disclosure-dropdown-item :item="menuItem">
- <template #list-item>
- <span class="gl-display-flex gl-flex-direction-column">
- <span>
- <span class="gl-font-weight-bold">
- {{ user.name }}
- </span>
- <gl-badge v-if="user.status.busy" size="sm" variant="warning">
- {{ $options.i18n.user.busy }}
- </gl-badge>
- </span>
-
- <span class="gl-text-gray-400">@{{ user.username }}</span>
-
- <span
- v-if="user.status.customized"
- ref="statusTooltipTarget"
- data-testid="user-menu-status"
- class="gl-display-flex gl-align-items-baseline gl-mt-2 gl-font-sm"
- >
- <gl-emoji :data-name="user.status.emoji" class="gl-mr-1" />
- <span v-safe-html="user.status.message_html" class="gl-text-truncate"></span>
- <gl-tooltip
- v-if="user.status.message_html"
- :target="() => $refs.statusTooltipTarget"
- boundary="viewport"
- placement="bottom"
- >
- <span v-safe-html="user.status.message_html"></span>
- </gl-tooltip>
- </span>
- </span>
- </template>
- </gl-disclosure-dropdown-item>
- </gl-disclosure-dropdown-group>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js
index 77bd8b4a734..e96dca3f365 100644
--- a/app/assets/javascripts/super_sidebar/constants.js
+++ b/app/assets/javascripts/super_sidebar/constants.js
@@ -59,4 +59,4 @@ export const DROPDOWN_Y_OFFSET = 4;
export const NAV_ITEM_LINK_ACTIVE_CLASS = 'gl-bg-t-gray-a-08';
-export const IMPERSONATING_OFFSET = 32;
+export const IMPERSONATING_OFFSET = 34;
diff --git a/app/assets/javascripts/super_sidebar/event_hub.js b/app/assets/javascripts/super_sidebar/event_hub.js
new file mode 100644
index 00000000000..e31806ad199
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/event_hub.js
@@ -0,0 +1,3 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index de16161efb5..f9e488ea5ee 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -33,6 +33,8 @@ const getTrialStatusWidgetData = (sidebarData) => {
companyName,
glmContent,
createHandRaiseLeadPath,
+ trackAction,
+ trackLabel,
} = convertObjectPropsToCamelCase(sidebarData.trial_status_popover_data_attrs);
return {
@@ -47,6 +49,8 @@ const getTrialStatusWidgetData = (sidebarData) => {
daysRemaining,
targetId,
createHandRaiseLeadPath,
+ trackAction,
+ trackLabel,
trialEndDate: new Date(trialEndDate),
user: { namespaceId, userName, firstName, lastName, companyName, glmContent },
};
diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js
index 97830a32d78..d2fb72adb85 100644
--- a/app/assets/javascripts/super_sidebar/utils.js
+++ b/app/assets/javascripts/super_sidebar/utils.js
@@ -59,19 +59,17 @@ const updateItemAccess = (
const neverAccessed = !lastAccessedOn;
const shouldUpdate = neverAccessed || Math.abs(now - lastAccessedOn) / FIFTEEN_MINUTES_IN_MS > 1;
- if (shouldUpdate && gon.features?.serverSideFrecentNamespaces) {
- try {
- axios({
- url: trackVisitsPath,
- method: 'POST',
- data: {
- type: namespace,
- id: contextItem.id,
- },
- });
- } catch (e) {
+ if (shouldUpdate) {
+ axios({
+ url: trackVisitsPath,
+ method: 'POST',
+ data: {
+ type: namespace,
+ id: contextItem.id,
+ },
+ }).catch((e) => {
Sentry.captureException(e);
- }
+ });
}
return {
diff --git a/app/assets/javascripts/terms/components/app.vue b/app/assets/javascripts/terms/components/app.vue
index 0ae97a47170..29099bcc366 100644
--- a/app/assets/javascripts/terms/components/app.vue
+++ b/app/assets/javascripts/terms/components/app.vue
@@ -5,7 +5,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import csrf from '~/lib/utils/csrf';
-import { trackTrialAcceptTerms } from '~/google_tag_manager';
+import { trackTrialAcceptTerms } from 'ee_else_ce/google_tag_manager';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
diff --git a/app/assets/javascripts/terraform/components/empty_state.vue b/app/assets/javascripts/terraform/components/empty_state.vue
index 551b5498571..5a71f0d66de 100644
--- a/app/assets/javascripts/terraform/components/empty_state.vue
+++ b/app/assets/javascripts/terraform/components/empty_state.vue
@@ -32,13 +32,14 @@ export default {
</script>
<template>
- <gl-empty-state :svg-path="image" :title="$options.i18n.title">
+ <gl-empty-state :svg-path="image" :svg-height="null" :title="$options.i18n.title">
<template #actions>
- <gl-button variant="confirm" :href="$options.docsUrl">
+ <gl-button variant="confirm" :href="$options.docsUrl" class="gl-mx-2 gl-mb-3">
{{ $options.i18n.buttonDoc }}</gl-button
>
<gl-button
v-gl-modal-directive="$options.COMMAND_MODAL_ID"
+ class="gl-mx-2 gl-mb-3"
data-testid="terraform-state-copy-init-command"
icon="copy-to-clipboard"
>{{ $options.i18n.buttonCopy }}</gl-button
diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue
index 234ac0505b2..7e55f56279e 100644
--- a/app/assets/javascripts/token_access/components/inbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue
@@ -30,7 +30,7 @@ export default {
'CICD|Allow CI job tokens from the following projects to access this project',
),
settingDisabledMessage: s__(
- 'CICD|Enable feature to allow job token access by the following projects.',
+ 'CICD|Enable feature to limit job token access, so only the projects in this list can access this project with a CI/CD job token.',
),
addProject: __('Add project'),
cancel: __('Cancel'),
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index 88b7f6d3532..46278152879 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -36,4 +36,3 @@ export const SERVICE_PING_SECURITY_CONFIGURATION_THREAT_MANAGEMENT_VISIT =
'users_visiting_security_configuration_threat_management';
export const SERVICE_PING_PIPELINE_SECURITY_VISIT = 'users_visiting_pipeline_security';
-export const USER_CONTEXT_SCHEMA = 'iglu:com.gitlab/user_context/jsonschema/1-0-0';
diff --git a/app/assets/javascripts/tracking/internal_events.js b/app/assets/javascripts/tracking/internal_events.js
index 9bd0200cad1..d5bc428934c 100644
--- a/app/assets/javascripts/tracking/internal_events.js
+++ b/app/assets/javascripts/tracking/internal_events.js
@@ -1,12 +1,10 @@
import API from '~/api';
-import getStandardContext from './get_standard_context';
import Tracking from './tracking';
import {
GITLAB_INTERNAL_EVENT_CATEGORY,
LOAD_INTERNAL_EVENTS_SELECTOR,
SERVICE_PING_SCHEMA,
- USER_CONTEXT_SCHEMA,
} from './constants';
import { Tracker } from './tracker';
import { InternalEventHandler, createInternalEventPayload } from './utils';
@@ -17,7 +15,7 @@ const InternalEvents = {
* @param {string} event
* @param {object} data
*/
- track_event(event, data = {}) {
+ trackEvent(event, data = {}) {
const { context, ...rest } = data;
const defaultContext = {
@@ -34,6 +32,7 @@ const InternalEvents = {
context: mergedContext,
...rest,
});
+ this.trackBrowserSDK(event);
},
/**
* Returns an implementation of this class in the form of
@@ -42,8 +41,8 @@ const InternalEvents = {
mixin() {
return {
methods: {
- track_event(event, data = {}) {
- InternalEvents.track_event(event, data);
+ trackEvent(event, data = {}) {
+ InternalEvents.trackEvent(event, data);
},
},
};
@@ -62,7 +61,10 @@ const InternalEvents = {
// eslint-disable-next-line no-param-reassign
parent.internalEventsTrackingBound = true;
- const handler = { name: 'click', func: (e) => InternalEventHandler(e, this.track_event) };
+ const handler = {
+ name: 'click',
+ func: (e) => InternalEventHandler(e, this.trackEvent.bind(this)),
+ };
parent.addEventListener(handler.name, handler.func);
return handler;
},
@@ -81,7 +83,7 @@ const InternalEvents = {
loadEvents.forEach((element) => {
const action = createInternalEventPayload(element);
if (action) {
- this.track_event(action);
+ this.trackEvent(action);
}
});
@@ -91,21 +93,24 @@ const InternalEvents = {
* Initialize browser sdk for product analytics
*/
initBrowserSDK() {
- const standardContext = getStandardContext();
-
if (window.glClient) {
window.glClient.setDocumentTitle('GitLab');
window.glClient.page({
title: 'GitLab',
- context: [
- {
- schema: USER_CONTEXT_SCHEMA,
- data: standardContext?.data || {},
- },
- ],
});
}
},
+ /**
+ * track events for Product Analytics
+ * @param {string} event
+ */
+ trackBrowserSDK(event) {
+ if (!Tracker.enabled()) {
+ return;
+ }
+
+ window.glClient?.track(event);
+ },
};
export default InternalEvents;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
index c49c1316b1b..e16ccdd35b9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
@@ -83,12 +83,6 @@ export default {
return btn.tooltipText;
},
- actionButtonQaSelector(btn) {
- if (btn.dataQaSelector) {
- return btn.dataQaSelector;
- }
- return 'mr_widget_extension_actions_button';
- },
},
};
</script>
@@ -105,7 +99,6 @@ export default {
:target="btn.target"
:class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
:data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
:data-method="btn.dataMethod"
:icon="btn.icon"
:data-testid="btn.testId || 'extension-actions-button'"
@@ -157,9 +150,8 @@ export default {
:title="setTooltip(btn)"
:href="btn.href"
:target="btn.target"
- :class="[{ 'gl-mr-1': index !== tertiaryButtons.length - 1 }, btn.class]"
+ :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
:data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
:data-method="btn.dataMethod"
:icon="btn.icon"
:data-testid="btn.testId || 'extension-actions-button'"
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 4ed470440cc..974b53caa15 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
@@ -24,10 +24,6 @@ export default {
GlSprintf,
},
mixins: [approvalsMixin, glFeatureFlagsMixin()],
- provide: {
- expandDetailsTooltip: __('Expand eligible approvers'),
- collapseDetailsTooltip: __('Collapse eligible approvers'),
- },
props: {
mr: {
type: Object,
@@ -248,6 +244,8 @@ export default {
is-collapsible
collapse-on-desktop
:collapsed="collapsed"
+ :expand-details-tooltip="__('Expand eligible approvers')"
+ :collapse-details-tooltip="__('Collapse eligible approvers')"
@toggle="() => $emit('toggle')"
>
<template v-if="$apollo.queries.approvals.loading">{{ $options.FETCH_LOADING }}</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
new file mode 100644
index 00000000000..303952c787e
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
@@ -0,0 +1,77 @@
+<script>
+import { __ } from '~/locale';
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+import conflictsStateQuery from '../../queries/states/conflicts.query.graphql';
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+export default {
+ name: 'MergeChecksConflicts',
+ components: {
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ apollo: {
+ state: {
+ query: conflictsStateQuery,
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data?.project?.mergeRequest,
+ },
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ mr: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ state: {},
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.state.loading;
+ },
+ userPermissions() {
+ return this.state.userPermissions;
+ },
+ showResolveButton() {
+ return (
+ this.mr.conflictResolutionPath &&
+ this.userPermissions.pushToSourceBranch &&
+ !this.state.sourceBranchProtected
+ );
+ },
+ tertiaryActionsButtons() {
+ if (this.state.shouldBeRebased) return [];
+
+ return [
+ {
+ text: __('Resolve locally'),
+ class: 'js-check-out-modal-trigger',
+ },
+ this.showResolveButton && {
+ text: __('Resolve conflicts'),
+ category: 'default',
+ href: this.mr.conflictResolutionPath,
+ },
+ ].filter((b) => b);
+ },
+ },
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ </merge-checks-message>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
new file mode 100644
index 00000000000..d0d749aa441
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
@@ -0,0 +1,44 @@
+<script>
+import StatusIcon from '../widget/status_icon.vue';
+
+const ICON_NAMES = {
+ failed: 'failed',
+ allowed_to_fail: 'neutral',
+ passed: 'success',
+};
+
+export default {
+ name: 'MergeChecksMessage',
+ components: {
+ StatusIcon,
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ mr: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ iconName() {
+ return ICON_NAMES[this.check.result];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-3 gl-pl-7">
+ <div class="gl-display-flex">
+ <status-icon :icon-name="iconName" :level="2" />
+ <div class="gl-w-full gl-min-w-0">
+ <div class="gl-display-flex">{{ check.failureReason }}</div>
+ </div>
+ <slot></slot>
+ </div>
+ </div>
+</template>
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 8290e7e9232..1829b674455 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
@@ -37,7 +37,7 @@ export default {
</script>
<template>
- <div class="deploy-heading gl-px-5">
+ <div class="deploy-heading gl-pl-5 gl-pr-4">
<div class="ci-widget media">
<div class="media-body">
<div class="deploy-body">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
new file mode 100644
index 00000000000..1c57226f887
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
@@ -0,0 +1,91 @@
+import createMockApollo from 'helpers/mock_apollo_helper';
+import mergeChecksQuery from '../queries/merge_checks.query.graphql';
+import conflictsStateQuery from '../queries/states/conflicts.query.graphql';
+import MergeChecks from './merge_checks.vue';
+
+const stylesheetsRequireCtx = require.context(
+ '../../../stylesheets',
+ true,
+ /(page_bundles\/merge_requests)\.scss$/,
+);
+
+stylesheetsRequireCtx('./page_bundles/merge_requests.scss');
+
+const defaultRender = (apolloProvider) => ({
+ components: { MergeChecks },
+ apolloProvider,
+ data() {
+ return { mr: { conflictResolutionPath: 'https://gitlab.com' } };
+ },
+ template: '<merge-checks :mr="mr" />',
+});
+
+const Template = ({ canMerge, failed, pushToSourceBranch }) => {
+ const requestHandlers = [
+ [
+ mergeChecksQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ userPermissions: { canMerge },
+ mergeChecks: [
+ {
+ failureReason: 'Unresolved discussions',
+ identifier: 'unresolved_discussions',
+ result: failed ? 'failed' : 'passed',
+ },
+ {
+ failureReason: 'Resolve conflicts',
+ identifier: 'conflicts',
+ result: failed ? 'failed' : 'passed',
+ },
+ ],
+ },
+ },
+ },
+ }),
+ ],
+ [
+ conflictsStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ shouldBeRebased: false,
+ sourceBranchProtected: false,
+ userPermissions: { pushToSourceBranch },
+ },
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender(apolloProvider);
+};
+
+const LoadingTemplate = () => {
+ const requestHandlers = [[mergeChecksQuery, () => new Promise(() => {})]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender(apolloProvider);
+};
+
+export const Default = Template.bind({});
+Default.args = { canMerge: true, failed: true, pushToSourceBranch: true };
+
+export const Loading = LoadingTemplate.bind({});
+Loading.args = {};
+
+export default {
+ title: 'vue_merge_request_widget/merge_checks',
+ component: MergeChecks,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
new file mode 100644
index 00000000000..fa84c0a4a6f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
@@ -0,0 +1,129 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { n__, __, sprintf } from '~/locale';
+import mergeRequestQueryVariablesMixin from '../mixins/merge_request_query_variables';
+import mergeChecksQuery from '../queries/merge_checks.query.graphql';
+import StateContainer from './state_container.vue';
+import BoldText from './bold_text.vue';
+
+const COMPONENTS = {
+ conflicts: () => import('./checks/conflicts.vue'),
+ default: () => import('./checks/message.vue'),
+};
+
+export default {
+ apollo: {
+ state: {
+ query: mergeChecksQuery,
+ skip() {
+ return !this.mr;
+ },
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data?.project?.mergeRequest,
+ },
+ },
+ components: {
+ GlSkeletonLoader,
+ StateContainer,
+ BoldText,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ collapsed: true,
+ state: {},
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.state.loading;
+ },
+ statusIcon() {
+ return this.failedChecks.length ? 'failed' : 'success';
+ },
+ summaryText() {
+ if (!this.failedChecks.length) {
+ return this.state?.userPermissions?.canMerge
+ ? __('%{boldStart}Ready to merge!%{boldEnd}')
+ : __(
+ '%{boldStart}Ready to merge by members who can write to the target branch.%{boldEnd}',
+ );
+ }
+
+ return sprintf(
+ n__(
+ '%{boldStart}Merge blocked:%{boldEnd} %{count} check failed',
+ '%{boldStart}Merge blocked:%{boldEnd} %{count} checks failed',
+ this.failedChecks.length,
+ ),
+ { count: this.failedChecks.length },
+ );
+ },
+ checks() {
+ return this.state.mergeChecks || [];
+ },
+ failedChecks() {
+ return this.checks.filter((c) => c.result === 'failed');
+ },
+ },
+ methods: {
+ toggleCollapsed() {
+ this.collapsed = !this.collapsed;
+ },
+ checkComponent(check) {
+ return COMPONENTS[check.identifier] || COMPONENTS.default;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <state-container
+ :is-loading="isLoading"
+ :status="statusIcon"
+ is-collapsible
+ collapse-on-desktop
+ :collapsed="collapsed"
+ :expand-details-tooltip="__('Expand merge checks')"
+ :collapse-details-tooltip="__('Collapse merge checks')"
+ @toggle="toggleCollapsed"
+ >
+ <template v-if="isLoading" #loading>
+ <gl-skeleton-loader :width="334" :height="24">
+ <rect x="0" y="0" width="24" height="24" rx="4" />
+ <rect x="32" y="2" width="302" height="20" rx="4" />
+ </gl-skeleton-loader>
+ </template>
+ <template v-else>
+ <bold-text :message="summaryText" />
+ </template>
+ </state-container>
+ <div
+ v-if="!collapsed"
+ class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-relative gl-bg-gray-10"
+ data-testid="merge-checks-full"
+ >
+ <div class="gl-px-5">
+ <component
+ :is="checkComponent(check)"
+ v-for="(check, index) in checks"
+ :key="index"
+ :class="{
+ 'gl-border-b-solid gl-border-b-1 gl-border-gray-100': index !== checks.length - 1,
+ }"
+ :check="check"
+ :mr="mr"
+ />
+ </div>
+ </div>
+ </div>
+</template>
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 bfcd4610379..2e104f2b93b 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
@@ -10,7 +10,7 @@ import {
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
import PipelineArtifacts from '~/ci/pipelines_page/components/pipelines_artifacts.vue';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
@@ -21,7 +21,7 @@ import { MT_MERGE_STRATEGY } from '../constants';
export default {
name: 'MRWidgetPipeline',
components: {
- CiIcon,
+ CiBadgeLink,
GlLink,
GlLoadingIcon,
GlIcon,
@@ -194,24 +194,23 @@ export default {
</p>
</template>
<template v-else-if="hasPipeline">
- <a :href="status.details_path" class="gl-align-self-start gl-mt-2 gl-mr-3">
- <ci-icon :status="status" :size="24" class="gl-display-flex" />
- </a>
+ <ci-badge-link
+ :status="status"
+ :href="status.details_path"
+ size="md"
+ :show-text="false"
+ class="gl-align-self-start gl-mt-2 gl-mr-3"
+ />
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
<div class="media-body">
<div
data-testid="pipeline-info-container"
- data-qa-selector="merge_request_pipeline_info_content"
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-justify-content-space-between"
>
<p class="mr-pipeline-title gl-m-0! gl-mr-3! gl-font-weight-bold gl-text-gray-900">
{{ pipeline.details.event_type_name }}
- <gl-link
- :href="pipeline.path"
- class="pipeline-id"
- data-testid="pipeline-id"
- data-qa-selector="pipeline_link"
+ <gl-link :href="pipeline.path" class="pipeline-id" data-testid="pipeline-id"
>#{{ pipeline.id }}</gl-link
>
{{ pipeline.details.status.label }}
@@ -240,7 +239,7 @@ export default {
{{ s__('Pipeline|for') }}
<gl-link
:href="pipeline.commit.commit_path"
- class="commit-sha gl-font-weight-normal"
+ class="commit-sha-container"
data-testid="commit-link"
>{{ pipeline.commit.short_id }}</gl-link
>
@@ -251,7 +250,7 @@ export default {
v-safe-html="sourceBranchLink"
:title="sourceBranch"
truncate-target="child"
- class="label-branch label-truncate gl-font-weight-normal"
+ class="label-branch label-truncate ref-container"
/>
</template>
<template v-if="finishedAt">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
index dd899701de0..2a18af90495 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
import { STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
import StatusIcon from './mr_widget_status_icon.vue';
import Actions from './action_buttons.vue';
@@ -13,14 +14,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: {
- expandDetailsTooltip: {
- default: '',
- },
- collapseDetailsTooltip: {
- default: '',
- },
- },
props: {
isCollapsible: {
type: Boolean,
@@ -57,6 +50,16 @@ export default {
required: false,
default: () => [],
},
+ expandDetailsTooltip: {
+ required: false,
+ type: String,
+ default: __('Expand merge details'),
+ },
+ collapseDetailsTooltip: {
+ required: false,
+ type: String,
+ default: __('Collapse merge details'),
+ },
},
computed: {
wrapperClasses() {
@@ -120,6 +123,7 @@ export default {
<gl-button
v-gl-tooltip
:title="collapsed ? expandDetailsTooltip : collapseDetailsTooltip"
+ :aria-label="collapsed ? expandDetailsTooltip : collapseDetailsTooltip"
:icon="collapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
category="tertiary"
size="small"
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 6299f0fcbb8..ec72b74daa2 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
@@ -75,7 +75,6 @@ export default {
actions.push({
text: this.cancelButtonText,
loading: this.isCancellingAutoMerge,
- dataQaSelector: 'cancel_auto_merge_button',
class: 'js-cancel-auto-merge',
testId: 'cancelAutomaticMergeButton',
onClick: () => this.cancelAutomaticMerge(),
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 742f5d4de14..122abc7d034 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
@@ -51,7 +51,6 @@ export default {
text: s__('mrWidget|Refresh now'),
onClick: () => this.refresh(),
testId: 'merge-request-failed-refresh-button',
- dataQaSelector: 'merge_request_error_content',
},
];
},
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 4d906f29cb0..4454718a647 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
@@ -67,7 +67,7 @@ export default {
actions.push({
text: this.revertLabel,
tooltipText: this.revertTitle,
- dataQaSelector: 'revert_button',
+ testId: 'revert-button',
onClick: () => this.openRevertModal(),
});
} else if (this.mr.revertInForkPath) {
@@ -75,7 +75,7 @@ export default {
text: this.revertLabel,
tooltipText: this.revertTitle,
href: this.mr.revertInForkPath,
- dataQaSelector: 'revert_button',
+ testId: 'revert-button',
dataMethod: 'post',
});
}
@@ -84,7 +84,7 @@ export default {
actions.push({
text: this.cherryPickLabel,
tooltipText: this.cherryPickTitle,
- dataQaSelector: 'cherry_pick_button',
+ testId: 'cherry-pick-button',
onClick: () => this.openCherryPickModal(),
});
} else if (this.mr.cherryPickInForkPath) {
@@ -92,7 +92,7 @@ export default {
text: this.cherryPickLabel,
tooltipText: this.cherryPickTitle,
href: this.mr.cherryPickInForkPath,
- dataQaSelector: 'cherry_pick_button',
+ testId: 'cherry-pick-button',
dataMethod: 'post',
});
}
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 415f58ea8e6..a4afdee4d49 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
@@ -230,7 +230,6 @@ export default {
v-if="!rebasingError"
class="gl-w-100 gl-md-w-auto gl-flex-grow-1 gl-ml-0! gl-text-body! gl-md-mr-3"
data-testid="rebase-message"
- data-qa-selector="no_fast_forward_message_content"
>
<bold-text :message="$options.i18n.rebaseError" />
</span>
@@ -247,7 +246,6 @@ export default {
:loading="isMakingRequest"
variant="confirm"
size="small"
- data-qa-selector="mr_rebase_button"
data-testid="standard-rebase-button"
class="gl-align-self-start"
@click="tryRebase"
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 0ce8389579d..ac434c5be4e 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
@@ -634,7 +634,6 @@ export default {
variant="confirm"
:disabled="isMergeButtonDisabled"
:loading="isMakingRequest"
- data-qa-selector="merge_button"
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
@@ -644,7 +643,6 @@ export default {
:disabled="isMergeButtonDisabled"
variant="confirm"
data-testid="merge-immediately-dropdown"
- data-qa-selector="merge_moment_dropdown"
toggle-class="btn-icon js-merge-moment"
>
<template #button-content>
@@ -655,7 +653,6 @@ export default {
icon-name="warning"
button-class="accept-merge-request"
data-testid="merge-immediately-button"
- data-qa-selector="merge_immediately_menu_item"
@click="handleMergeImmediatelyButtonClick"
>
{{ __('Merge immediately') }}
@@ -692,7 +689,7 @@ export default {
<div
v-else
class="gl-w-full gl-order-n1 mr-widget-merge-details"
- data-qa-selector="merged_status_content"
+ data-testid="merged-status-content"
>
<p v-if="showMergeDetailsHeader" class="gl-mb-2 gl-text-gray-900">
{{ __('Merge details') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
index 9da754d01fc..00383418f2d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
@@ -32,7 +32,7 @@ export default {
>
<span
class="gl-md-mr-3 gl-flex-grow-1 gl-ml-0! gl-text-body!"
- data-qa-selector="head_mismatch_content"
+ data-testid="head-mismatch-content"
>
<bold-text :message="$options.i18n.I18N_SHA_MISMATCH.warningMessage" />
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index 97ef96fe382..f1bd5bb25bb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -46,7 +46,7 @@ export default {
:disabled="isDisabled"
name="squash"
class="js-squash-checkbox gl-mr-2"
- data-qa-selector="squash_checkbox"
+ data-testid="squash-checkbox"
:title="tooltipTitle"
@change="(checked) => $emit('input', checked)"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
index 9dd4e76befe..5b7657f15d9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
@@ -1,12 +1,18 @@
<script>
-import { GlButton, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
-import { sprintf, __ } from '~/locale';
+import {
+ GlButton,
+ GlDisclosureDropdown,
+ GlIcon,
+ GlLoadingIcon,
+ GlTooltipDirective,
+} from '@gitlab/ui';
export default {
components: {
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlIcon,
+ GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -29,10 +35,22 @@ export default {
};
},
computed: {
- dropdownLabel() {
- if (!this.widget) return undefined;
-
- return sprintf(__('%{widget} options'), { widget: this.widget });
+ dropdownItems() {
+ return this.tertiaryButtons.map((button) => {
+ return {
+ text: button.text,
+ href: button.href,
+ action: () => this.onClickAction(button),
+ icon: button.icon || button.iconName,
+ loading: button.loading,
+ extraAttrs: {
+ dataClipboardText: button.dataClipboardText,
+ dataMethod: button.dataMethod,
+ target: button.target,
+ disabled: button.disabled,
+ },
+ };
+ });
},
},
methods: {
@@ -62,44 +80,31 @@ export default {
return btn.tooltipText;
},
- actionButtonQaSelector(btn) {
- if (btn.dataQaSelector) {
- return btn.dataQaSelector;
- }
- return 'mr_widget_extension_actions_button';
- },
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-flex-start">
- <gl-dropdown
- v-gl-tooltip
- :title="__('Options')"
- :text="dropdownLabel"
+ <gl-disclosure-dropdown
+ :items="dropdownItems"
icon="ellipsis_v"
no-caret
category="tertiary"
- right
- lazy
+ placement="right"
text-sr-only
size="small"
toggle-class="gl-p-2!"
class="gl-display-block gl-md-display-none!"
>
- <gl-dropdown-item
- v-for="(btn, index) in tertiaryButtons"
- :key="index"
- :href="btn.href"
- :target="btn.target"
- :data-clipboard-text="btn.dataClipboardText"
- :data-method="btn.dataMethod"
- @click="onClickAction(btn)"
- >
- {{ btn.text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item="{ item }">
+ <span class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
+ {{ item.text }}
+ <gl-loading-icon v-if="item.loading" size="sm" />
+ <gl-icon v-else-if="item.icon" :name="item.icon" />
+ </span>
+ </template>
+ </gl-disclosure-dropdown>
<gl-button
v-for="(btn, index) in tertiaryButtons"
:id="btn.id"
@@ -110,9 +115,8 @@ export default {
:target="btn.target"
:class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
:data-clipboard-text="btn.dataClipboardText"
- :data-qa-selector="actionButtonQaSelector(btn)"
:data-method="btn.dataMethod"
- :icon="btn.icon"
+ :icon="btn.icon || btn.iconName"
:data-testid="btn.testId || 'extension-actions-button'"
:variant="btn.variant || 'confirm'"
:loading="btn.loading"
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index e8b97098a2b..5e9b72e13cf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -29,6 +29,8 @@ export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
gl.mrWidgetData.defaultAvatarUrl = gon.default_avatar_url;
+ const dismissalDescriptions = JSON.parse(gl.mrWidgetData.dismissal_descriptions || '{}');
+
// 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.
@@ -43,6 +45,8 @@ export default () => {
canCreatePipelineInTargetProject: parseBoolean(
gl.mrWidgetData.can_create_pipeline_in_target_project,
),
+ commitPathTemplate: gl.mrWidgetData.commit_path_template,
+ dismissalDescriptions,
},
...MrWidgetOptions,
apolloProvider,
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 175a0b0563f..02d73cf9cbd 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
@@ -153,10 +153,6 @@ export default {
},
},
mixins: [mergeRequestQueryVariablesMixin],
- provide: {
- expandDetailsTooltip: __('Expand merge details'),
- collapseDetailsTooltip: __('Collapse merge details'),
- },
props: {
mrData: {
type: Object,
@@ -576,7 +572,7 @@ export default {
</mr-widget-alert-message>
</div>
- <div class="mr-widget-section" data-qa-selector="mr_widget_content">
+ <div class="mr-widget-section" data-testid="mr-widget-content">
<component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge
v-if="mr.commitsCount"
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql
new file mode 100644
index 00000000000..6b602a0095c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql
@@ -0,0 +1,12 @@
+query mergeChecks($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ userPermissions {
+ canMerge
+ }
+ mergeChecks @client
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
index faf21b28f86..a4c42070530 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/conflicts.query.graphql
@@ -5,6 +5,9 @@ query workInProgress($projectPath: ID!, $iid: String!) {
id
shouldBeRebased
sourceBranchProtected
+ userPermissions {
+ pushToSourceBranch
+ }
}
}
}
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 bb74f82145f..a1b86c86979 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
@@ -2,7 +2,7 @@ import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_ke
import { STATUS_CLOSED, STATUS_MERGED, STATUS_OPEN } from '~/issues/constants';
import { formatDate, getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
import { machine } from '~/lib/utils/finite_state_machine';
-import { badgeState } from '~/merge_requests/components/merge_request_status_badge.vue';
+import { badgeState } from '~/merge_requests/components/merge_request_header.vue';
import {
MTWPS_MERGE_STRATEGY,
MT_MERGE_STRATEGY,
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
index 8d2ef20b381..3855e4fc078 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -11,10 +11,10 @@ export default {
'AlertManagement|There was an error while updating the status of the alert.',
),
UPDATE_ALERT_STATUS_INSTRUCTION: s__('AlertManagement|Please try again.'),
+ ASSIGN_STATUS_HEADER: s__('AlertManagement|Assign status'),
},
components: {
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
},
inject: {
trackAlertStatusUpdateOptions: {
@@ -44,10 +44,20 @@ export default {
default: () => PAGE_CONFIG.OPERATIONS.STATUSES,
},
},
+ data() {
+ return {
+ alertStatus: this.alert.status,
+ };
+ },
computed: {
dropdownClass() {
- // eslint-disable-next-line no-nested-ternary
- return this.isSidebar ? (this.isDropdownShowing ? 'show' : 'gl-display-none') : '';
+ return this.isSidebar && !this.isDropdownShowing ? 'gl-display-none' : '';
+ },
+ items() {
+ return Object.entries(this.statuses).map(([value, text]) => ({ value, text }));
+ },
+ headerText() {
+ return this.isSidebar ? this.$options.i18n.ASSIGN_STATUS_HEADER : '';
},
},
methods: {
@@ -97,30 +107,15 @@ export default {
<template>
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
- <gl-dropdown
+ <gl-collapsible-listbox
ref="dropdown"
- right
- :text="statuses[alert.status]"
- class="w-100"
- toggle-class="dropdown-menu-toggle"
- @keydown.esc.native="$emit('hide-dropdown')"
- @hide="$emit('hide-dropdown')"
- >
- <p v-if="isSidebar" class="gl-dropdown-header-top" data-testid="dropdown-header">
- {{ s__('AlertManagement|Assign status') }}
- </p>
- <div class="dropdown-content dropdown-body">
- <gl-dropdown-item
- v-for="(label, field) in statuses"
- :key="field"
- data-testid="statusDropdownItem"
- :active="field === alert.status"
- :active-class="'is-active'"
- @click="updateAlertStatus(field)"
- >
- {{ label }}
- </gl-dropdown-item>
- </div>
- </gl-dropdown>
+ v-model="alertStatus"
+ placement="right"
+ :header-text="headerText"
+ :items="items"
+ block
+ @hidden="$emit('hide-dropdown')"
+ @select="updateAlertStatus"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
index c512585b980..7b099516c5b 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
@@ -58,9 +58,10 @@ export default {
},
toggleFormDropdown() {
this.isDropdownShowing = !this.isDropdownShowing;
- const { dropdown } = this.$refs.status.$refs.dropdown.$refs;
+ const { dropdown } = this.$refs.status.$refs;
+
if (dropdown && this.isDropdownShowing) {
- dropdown.show();
+ dropdown.open();
}
},
handleUpdating(isMutationInProgress) {
diff --git a/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue b/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue
index e8d33b5538e..9cac176a06f 100644
--- a/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue
+++ b/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue
@@ -1,10 +1,10 @@
<script>
-import { GlBadge, GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale';
+import HoverBadge from './hover_badge.vue';
export default {
name: 'BetaBadge',
- components: { GlBadge, GlPopover },
+ components: { HoverBadge },
i18n: {
badgeLabel: s__('BetaBadge|Beta'),
popoverTitle: s__("BetaBadge|What's Beta?"),
@@ -41,27 +41,16 @@ export default {
</script>
<template>
- <div>
- <gl-badge ref="badge" href="#" :size="size" variant="neutral" class="gl-cursor-pointer">{{
- $options.i18n.badgeLabel
- }}</gl-badge>
- <gl-popover
- triggers="hover focus click"
- :show-close-button="true"
- :target="target"
- :title="$options.i18n.popoverTitle"
- data-testid="beta-badge"
- >
- <p>{{ $options.i18n.descriptionParagraph }}</p>
+ <hover-badge :label="$options.i18n.badgeLabel" :size="size" :title="$options.i18n.popoverTitle">
+ <p>{{ $options.i18n.descriptionParagraph }}</p>
- <p class="gl-mb-0">{{ $options.i18n.listIntroduction }}</p>
+ <p class="gl-mb-0">{{ $options.i18n.listIntroduction }}</p>
- <ul class="gl-pl-4">
- <li>{{ $options.i18n.listItemStability }}</li>
- <li>{{ $options.i18n.listItemDataLoss }}</li>
- <li>{{ $options.i18n.listItemReasonableEffort }}</li>
- <li>{{ $options.i18n.listItemNearCompletion }}</li>
- </ul>
- </gl-popover>
- </div>
+ <ul class="gl-pl-4">
+ <li>{{ $options.i18n.listItemStability }}</li>
+ <li>{{ $options.i18n.listItemDataLoss }}</li>
+ <li>{{ $options.i18n.listItemReasonableEffort }}</li>
+ <li>{{ $options.i18n.listItemNearCompletion }}</li>
+ </ul>
+ </hover-badge>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/badges/experiment_badge.stories.js b/app/assets/javascripts/vue_shared/components/badges/experiment_badge.stories.js
new file mode 100644
index 00000000000..8e964c9bdf8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/badges/experiment_badge.stories.js
@@ -0,0 +1,24 @@
+import ExperimentBadge from './experiment_badge.vue';
+
+export default {
+ component: ExperimentBadge,
+ title: 'vue_shared/experiment-badge',
+};
+
+const template = `
+ <div style="height:600px;" class="gl-display-flex gl-justify-content-center gl-align-items-center">
+ <experiment-badge :size="size" />
+ </div>
+ `;
+
+const Template = (args, { argTypes }) => ({
+ components: { ExperimentBadge },
+ data() {
+ return { value: args.value };
+ },
+ props: Object.keys(argTypes),
+ template,
+});
+
+export const Default = Template.bind({});
+Default.args = {};
diff --git a/app/assets/javascripts/vue_shared/components/badges/experiment_badge.vue b/app/assets/javascripts/vue_shared/components/badges/experiment_badge.vue
new file mode 100644
index 00000000000..26bae71ddb8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/badges/experiment_badge.vue
@@ -0,0 +1,43 @@
+<script>
+import { s__ } from '~/locale';
+import HoverBadge from './hover_badge.vue';
+
+export default {
+ name: 'ExperimentBadge',
+ components: { HoverBadge },
+ i18n: {
+ badgeLabel: s__('ExperimentBadge|Experiment'),
+ popoverTitle: s__("ExperimentBadge|What's an Experiment?"),
+ descriptionParagraph: s__(
+ "ExperimentBadge|An Experiment is a feature that's in the process of being developed. It's not production-ready. We encourage users to try Experimental features and provide feedback.",
+ ),
+ listIntroduction: s__('ExperimentBadge|An Experiment:'),
+ listItemStability: s__('ExperimentBadge|May be unstable.'),
+ listItemDataLoss: s__('ExperimentBadge|Can cause data loss.'),
+ listItemNoSupport: s__('ExperimentBadge|Has no support and might not be documented.'),
+ listItemCanBeRemoved: s__('ExperimentBadge|Can be removed at any time.'),
+ },
+ props: {
+ size: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
+ },
+};
+</script>
+
+<template>
+ <hover-badge :label="$options.i18n.badgeLabel" :size="size" :title="$options.i18n.popoverTitle">
+ <p>{{ $options.i18n.descriptionParagraph }}</p>
+
+ <p class="gl-mb-0">{{ $options.i18n.listIntroduction }}</p>
+
+ <ul class="gl-pl-4">
+ <li>{{ $options.i18n.listItemStability }}</li>
+ <li>{{ $options.i18n.listItemDataLoss }}</li>
+ <li>{{ $options.i18n.listItemNoSupport }}</li>
+ <li>{{ $options.i18n.listItemCanBeRemoved }}</li>
+ </ul>
+ </hover-badge>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/badges/hover_badge.vue b/app/assets/javascripts/vue_shared/components/badges/hover_badge.vue
new file mode 100644
index 00000000000..351c7bd9da0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/badges/hover_badge.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlBadge, GlPopover } from '@gitlab/ui';
+
+export default {
+ name: 'HoverBadge',
+ components: { GlBadge, GlPopover },
+ props: {
+ label: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ size: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
+ },
+ methods: {
+ target() {
+ /**
+ * BVPopover retrieves the target during the `beforeDestroy` hook to deregister attached
+ * events. Since during `beforeDestroy` refs are `undefined`, it throws a warning in the
+ * console because we're trying to access the `$el` property of `undefined`. Optional
+ * chaining is not working in templates, which is why the method is used.
+ *
+ * See more on https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49628#note_464803276
+ */
+ return this.$refs.badge?.$el;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-badge ref="badge" href="#" :size="size" variant="neutral" class="gl-cursor-pointer">{{
+ label
+ }}</gl-badge>
+ <gl-popover
+ triggers="hover focus click"
+ :show-close-button="true"
+ :target="target"
+ :title="title"
+ >
+ <slot></slot>
+ </gl-popover>
+ </div>
+</template>
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 27bdcc69120..b52752d7e2f 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
@@ -41,7 +41,6 @@ export default {
mounted() {
this.renderRemainingMarkup();
handleBlobRichViewer(this.$refs.content, this.type);
- handleLocationHash();
},
methods: {
optimizeMarkupRendering() {
@@ -76,8 +75,7 @@ export default {
* */
if (!this.isMarkup || !this.remainingContent.length) {
- this.$emit(CONTENT_LOADED_EVENT);
- this.isLoading = false;
+ this.onContentLoaded();
return;
}
@@ -89,11 +87,15 @@ export default {
setTimeout(() => {
fileContent.append(...content);
if (nextChunkEnd < this.remainingContent.length) return;
- this.$emit(CONTENT_LOADED_EVENT);
- this.isLoading = false;
+ this.onContentLoaded();
}, i);
}
},
+ onContentLoaded() {
+ this.$emit(CONTENT_LOADED_EVENT);
+ handleLocationHash();
+ this.isLoading = false;
+ },
},
safeHtmlConfig: {
ADD_TAGS: ['gl-emoji', 'copy-code'],
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 1f45b4c5c9d..abbeac0e098 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -57,16 +57,29 @@ export default {
return badgeSizeOptions[value] !== undefined;
},
},
+ showTooltip: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ useLink: {
+ type: Boolean,
+ default: true,
+ required: false,
+ },
},
computed: {
- isSmallBadgeSize() {
- return this.size === badgeSizeOptions.sm;
+ isNotLargeBadgeSize() {
+ return this.size !== badgeSizeOptions.lg;
},
title() {
- return !this.showText ? this.status?.text : '';
+ return this.showTooltip && !this.showText ? this.status?.text : '';
},
detailsPath() {
// For now, this can either come from graphQL with camelCase or REST API in snake_case
+ if (!this.useLink) {
+ return null;
+ }
return this.status.detailsPath || this.status.details_path;
},
badgeStyles() {
@@ -121,7 +134,7 @@ export default {
<template>
<gl-badge
v-gl-tooltip
- :class="{ 'gl-pl-2': isSmallBadgeSize }"
+ :class="{ 'gl-px-2': !showText && isNotLargeBadgeSize }"
:title="title"
:href="detailsPath"
:size="size"
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown.vue
index fa7c5bc1978..066b761ac9b 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown.vue
@@ -47,13 +47,13 @@ export default {
v-if="sshLink"
:label="$options.labels.ssh"
:link="sshLink"
- qa-selector="copy_ssh_url_button"
+ test-id="copy-ssh-url-button"
/>
<clone-dropdown-item
v-if="httpLink"
:label="httpLabel"
:link="httpLink"
- qa-selector="copy_http_url_button"
+ test-id="copy-http-url-button"
/>
</gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue
index 0e322ebc686..6980e19733a 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue
@@ -27,7 +27,7 @@ export default {
type: String,
required: true,
},
- qaSelector: {
+ testId: {
type: String,
required: true,
},
@@ -45,7 +45,7 @@ export default {
:title="$options.copyURLTooltip"
:aria-label="$options.copyURLTooltip"
:data-clipboard-text="link"
- :data-qa-selector="qaSelector"
+ :data-testid="testId"
icon="copy-to-clipboard"
class="gl-display-inline-flex"
/>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
index b34a6b11092..1f5896204ee 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -144,7 +144,7 @@ export default {
</slot>
</template>
<slot name="default">
- <gl-dropdown-form class="gl-relative gl-min-h-7" data-qa-selector="labels_dropdown_content">
+ <gl-dropdown-form class="gl-relative gl-min-h-7" data-testid="labels-dropdown-content">
<gl-loading-icon
v-if="isLoading"
size="lg"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index 5a7382bcd7c..23de8dd5596 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -262,6 +262,7 @@ export default {
{{ __('No matches found') }}
</gl-dropdown-text>
<gl-dropdown-text v-else-if="hasFetched">{{ __('No suggestions found') }}</gl-dropdown-text>
+ <slot name="footer"></slot>
</template>
</gl-filtered-search-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
index c294c23abfc..4601287b417 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
@@ -4,6 +4,8 @@ import { compact } from 'lodash';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
+import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -41,6 +43,12 @@ export default {
preloadedUsers() {
return this.config.preloadedUsers || [];
},
+ namespace() {
+ return this.config.isProject ? WORKSPACE_PROJECT : WORKSPACE_GROUP;
+ },
+ fetchUsersQuery() {
+ return this.config.fetchUsers ? this.config.fetchUsers : this.fetchUsersBySearchTerm;
+ },
},
methods: {
getActiveUser(users, data) {
@@ -49,11 +57,19 @@ export default {
getAvatarUrl(user) {
return user.avatarUrl || user.avatar_url;
},
+ fetchUsersBySearchTerm(search) {
+ return this.$apollo
+ .query({
+ query: usersAutocompleteQuery,
+ variables: { fullPath: this.config.fullPath, search, isProject: this.config.isProject },
+ })
+ .then(({ data }) => data[this.namespace]?.autocompleteUsers);
+ },
fetchUsers(searchTerm) {
this.loading = true;
const fetchPromise = this.config.fetchPath
? this.config.fetchUsers(this.config.fetchPath, searchTerm)
- : this.config.fetchUsers(searchTerm);
+ : this.fetchUsersQuery(searchTerm);
fetchPromise
.then((res) => {
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
index ebc6b2cd740..d97f1ae6135 100644
--- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
@@ -156,7 +156,7 @@ export default {
<gl-form-input
ref="input"
:readonly="readonly"
- :size="size"
+ :width="size"
class="gl-font-monospace! gl-cursor-default!"
v-bind="formInputGroupProps"
:value="value"
@@ -183,7 +183,7 @@ export default {
v-if="showCopyButton"
:text="value"
:title="copyButtonTitle"
- data-qa-selector="clipboard_button"
+ data-testid="clipboard-button"
@click="handleCopyButtonClick"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/incubation/pagination.vue b/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
index b5afe92316a..6b70e9f3ed9 100644
--- a/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/incubation/pagination.vue
@@ -57,6 +57,7 @@ export default {
:next-text="$options.i18n.nextPageButtonLabel"
:prev-button-link="previousPageLink"
:next-button-link="nextPageLink"
+ class="gl-mt-4"
/>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index 05ce007e615..4ebd8861a67 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -60,7 +60,7 @@ export default {
<template>
<gl-disclosure-dropdown
- data-qa-selector="apply_suggestion_dropdown"
+ data-testid="apply-suggestion-dropdown"
fluid-width
placement="right"
size="small"
@@ -81,7 +81,7 @@ export default {
class="apply-suggestions-input-min-width"
:placeholder="defaultCommitMessage"
submit-on-enter
- data-qa-selector="commit_message_field"
+ data-testid="commit-message-field"
@submit="onApply"
/>
@@ -93,7 +93,7 @@ export default {
class="gl-w-auto! gl-mt-3 gl-align-self-end"
category="primary"
variant="confirm"
- data-qa-selector="commit_with_custom_message_button"
+ data-testid="commit-with-custom-message-button"
@click="onApply"
>
{{ __('Apply') }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index f7f5ccdbf31..d99b90fa561 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -64,8 +64,8 @@ export default {
const savedReply = this.savedReplies.find((r) => r.id === id);
if (savedReply) {
this.$emit('select', savedReply.content);
- this.track_event(TRACKING_SAVED_REPLIES_USE);
- this.track_event(
+ this.trackEvent(TRACKING_SAVED_REPLIES_USE);
+ this.trackEvent(
isInMr ? TRACKING_SAVED_REPLIES_USE_IN_MR : TRACKING_SAVED_REPLIES_USE_IN_OTHER,
);
}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
index 2426a917a53..1327436a9b4 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue
@@ -1,16 +1,10 @@
<script>
-import { GlButton, GlPopover, GlLink } from '@gitlab/ui';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import RICH_TEXT_EDITOR_ILLUSTRATION from '../../../../images/callouts/rich_text_editor_illustration.svg?url';
-import { counter } from './utils';
export default {
components: {
GlButton,
- GlLink,
- GlPopover,
- UserCalloutDismisser,
},
props: {
value: {
@@ -18,15 +12,7 @@ export default {
required: true,
},
},
- data() {
- return {
- counter: counter(),
- };
- },
computed: {
- showPromoPopover() {
- return this.markdownEditorSelected && this.counter === 0;
- },
markdownEditorSelected() {
return this.value === 'markdown';
},
@@ -36,84 +22,19 @@ export default {
: __('Switch to plain text editing');
},
},
- methods: {
- switchEditorType(insertTemplate = false) {
- this.$emit('switch', insertTemplate);
- },
- },
richTextEditorButtonId: 'switch-to-rich-text-editor',
- RICH_TEXT_EDITOR_ILLUSTRATION,
};
</script>
<template>
<div class="content-editor-switcher gl-display-inline-flex gl-align-items-center">
- <user-callout-dismisser feature-name="rich_text_editor">
- <template #default="{ dismiss, shouldShowCallout }">
- <div>
- <gl-popover
- :target="$options.richTextEditorButtonId"
- :show="Boolean(showPromoPopover && shouldShowCallout)"
- show-close-button
- :css-classes="['rich-text-promo-popover gl-p-2']"
- triggers="manual"
- data-testid="rich-text-promo-popover"
- @close-button-clicked="dismiss"
- >
- <img
- :src="$options.RICH_TEXT_EDITOR_ILLUSTRATION"
- :alt="''"
- class="rich-text-promo-popover-illustration"
- width="280"
- height="130"
- />
- <h5 class="gl-mt-3 gl-mb-3">{{ __('Writing just got easier') }}</h5>
- <p class="gl-m-0">
- {{
- __(
- 'Use the new rich text editor to see your text and tables fully formatted as you type. No need to remember any formatting syntax, or switch between preview and editing modes!',
- )
- }}
- </p>
- <gl-link
- class="gl-button btn btn-confirm block gl-mb-2 gl-mt-4"
- variant="confirm"
- category="primary"
- target="_blank"
- block
- @click="
- switchEditorType(showPromoPopover);
- dismiss();
- "
- >
- {{ __('Try the rich text editor now') }}
- </gl-link>
- </gl-popover>
- <gl-button
- :id="$options.richTextEditorButtonId"
- class="btn btn-default btn-sm gl-button btn-default-tertiary gl-font-sm! gl-text-secondary! gl-px-4!"
- data-qa-selector="editing_mode_switcher"
- @click="
- switchEditorType();
- dismiss();
- "
- >{{ text }}</gl-button
- >
- </div>
- </template>
- </user-callout-dismisser>
+ <gl-button
+ :id="$options.richTextEditorButtonId"
+ size="small"
+ category="tertiary"
+ class="gl-font-sm! gl-text-secondary! gl-px-4!"
+ data-testid="editing-mode-switcher"
+ @click="$emit('switch')"
+ >{{ text }}</gl-button
+ >
</div>
</template>
-<style>
-.rich-text-promo-popover {
- box-shadow: 0 0 18px -1.9px rgba(119, 89, 194, 0.16), 0 0 12.9px -1.7px rgba(119, 89, 194, 0.16),
- 0 0 9.2px -1.4px rgba(119, 89, 194, 0.16), 0 0 6.4px -1.1px rgba(119, 89, 194, 0.16),
- 0 0 4.5px -0.8px rgba(119, 89, 194, 0.16), 0 0 3px -0.6px rgba(119, 89, 194, 0.16),
- 0 0 1.8px -0.3px rgba(119, 89, 194, 0.16), 0 0 0.6px rgba(119, 89, 194, 0.16);
- z-index: 999;
-}
-
-.rich-text-promo-popover-illustration {
- width: calc(100% + 32px);
- margin: -32px -16px 0;
-}
-</style>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index a26f8f71601..24211833026 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -12,6 +12,8 @@ import { __, sprintf } from '~/locale';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import { MARKDOWN_EDITOR_READY_EVENT } from '~/vue_shared/constants';
+import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
@@ -259,7 +261,8 @@ export default {
},
mounted() {
// GLForm class handles all the toolbar buttons
- return new GLForm(
+ // eslint-disable-next-line no-new
+ new GLForm(
$(this.$refs['gl-form']),
{
emojis: this.enableAutocomplete,
@@ -276,6 +279,8 @@ export default {
true,
this.autocompleteDataSources,
);
+
+ markdownEditorEventHub.$emit(MARKDOWN_EDITOR_READY_EVENT);
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('glForm');
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 286a1b87ad0..741bdfd211b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -263,7 +263,7 @@ export default {
<gl-button
v-if="enablePreview"
data-testid="preview-toggle"
- value="preview"
+ :value="previewMarkdown ? 'preview' : 'edit'"
:label="$options.i18n.previewTabTitle"
class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal! gl-mr-2"
size="small"
@@ -281,7 +281,7 @@ export default {
:tag-content="lineContent"
tracking-property="codeSuggestion"
icon="doc-code"
- data-qa-selector="suggestion_button"
+ data-testid="suggestion-button"
class="js-suggestion-btn"
@click="handleSuggestDismissed"
/>
@@ -305,7 +305,7 @@ export default {
variant="confirm"
category="primary"
size="small"
- data-qa-selector="dismiss_suggestion_popover_button"
+ data-testid="dismiss-suggestion-popover-button"
@click="handleSuggestDismissed"
>
{{ __('Got it') }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index fc7e0a7c732..4a3c3cf0053 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -248,6 +248,13 @@ export default {
});
}
},
+ onKeydown(event) {
+ const isModifierKey = event.ctrlKey || event.metaKey;
+ if (isModifierKey && event.key === 'k') {
+ event.preventDefault();
+ }
+ this.$emit('keydown', event);
+ },
},
EDITING_MODE_KEY,
};
@@ -292,7 +299,7 @@ export default {
class="note-textarea js-gfm-input markdown-area"
dir="auto"
:data-supports-quick-actions="supportsQuickActions"
- :data-qa-selector="formFieldProps['data-qa-selector'] || 'markdown_editor_form_field'"
+ :data-testid="formFieldProps['data-testid'] || 'markdown-editor-form-field'"
:disabled="disabled"
@input="updateMarkdownFromMarkdownField"
@keydown="$emit('keydown', $event)"
@@ -317,13 +324,13 @@ export default {
:code-suggestions-config="codeSuggestionsConfig"
@initialized="setEditorAsAutofocused"
@change="updateMarkdownFromContentEditor"
- @keydown="$emit('keydown', $event)"
+ @keydown="onKeydown"
@enableMarkdownEditor="onEditingModeChange('markdownField')"
/>
<input
v-bind="formFieldProps"
:value="markdown"
- data-qa-selector="markdown_editor_form_field"
+ data-testid="markdown-editor-form-field"
type="hidden"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
index 6c2f084591e..f7fb1339bbc 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
+++ b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
@@ -105,7 +105,6 @@ export function mountMarkdownEditor(options = {}) {
return h(MarkdownEditor, {
props: {
setFacade,
- enableContentEditor: Boolean(gon.features?.contentEditorOnIssues),
value: formFieldValue,
renderMarkdownPath,
markdownDocsPath,
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 8a0ca8ebac1..a822e2a6151 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
@@ -144,13 +144,13 @@ export default {
<gl-icon name="question-o" css-classes="link-highlight" />
</a>
</div>
- <gl-badge v-if="isApplied" variant="success" data-qa-selector="applied_badge">
+ <gl-badge v-if="isApplied" variant="success" data-testid="applied-badge">
{{ __('Applied') }}
</gl-badge>
<div
v-else-if="isApplying"
class="gl-display-flex gl-align-items-center text-secondary"
- data-qa-selector="applying_badge"
+ data-testid="applying-badge"
>
<gl-loading-icon size="sm" class="gl-align-items-center gl-justify-content-center gl-mr-3" />
<span>{{ applyingSuggestionsMessage }}</span>
@@ -169,7 +169,7 @@ export default {
<div v-else-if="!isDisableButton && suggestionsCount > 1">
<gl-button
class="btn-inverted js-add-to-batch-btn btn-grouped"
- data-qa-selector="add_suggestion_batch_button"
+ data-testid="add-suggestion-batch-button"
:disabled="isDisableButton"
size="small"
@click="addSuggestionToBatch"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index a4516fae73d..c0c8c4735e7 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -2,8 +2,6 @@
<script>
import { GlButton, GlLoadingIcon, GlSprintf, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { updateText } from '~/lib/utils/text_markdown';
-import { __, sprintf } from '~/locale';
-import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
import EditorModeSwitcher from './editor_mode_switcher.vue';
export default {
@@ -56,23 +54,6 @@ export default {
});
}
},
- handleEditorModeChanged(isFirstSwitch) {
- if (isFirstSwitch) {
- this.insertIntoTextarea(
- __(`### Rich text editor`),
- '',
- sprintf(
- __(
- 'Try out **styling** _your_ content right here or read the [direction](%{directionUrl}).',
- ),
- {
- directionUrl: `${PROMO_URL}/direction/plan/knowledge/content_editor/`,
- },
- ),
- );
- }
- this.$emit('enableContentEditor');
- },
},
};
</script>
@@ -91,7 +72,7 @@ export default {
v-if="showEditorModeSwitcher"
size="small"
value="markdown"
- @switch="handleEditorModeChanged"
+ @switch="$emit('enableContentEditor')"
/>
<div class="gl-display-flex">
<div v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32 gl-mr-3">
@@ -152,6 +133,7 @@ export default {
category="tertiary"
size="small"
:title="__('Markdown is supported')"
+ :aria-label="__('Markdown is supported')"
class="gl-px-3!"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/utils.js b/app/assets/javascripts/vue_shared/components/markdown/utils.js
deleted file mode 100644
index 0227d5a0fbc..00000000000
--- a/app/assets/javascripts/vue_shared/components/markdown/utils.js
+++ /dev/null
@@ -1,7 +0,0 @@
-let i = 0;
-
-export const counter = () => {
- const n = i;
- i += 1;
- return n;
-};
diff --git a/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue b/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue
index 7871721f38b..5c6766bbe45 100644
--- a/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/mr_more_dropdown.vue
@@ -19,7 +19,6 @@ import MergeRequest from '~/merge_request';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
Vue.use(VueApollo);
@@ -50,7 +49,6 @@ export default {
GlDisclosureDropdownGroup,
SidebarSubscriptionsWidget,
AbuseCategorySelector,
- NewHeaderActionsPopover,
SummaryNotesToggle: () =>
import('ee_component/merge_requests/components/summary_notes_toggle.vue'),
},
@@ -143,6 +141,9 @@ export default {
isMovedMrSidebar() {
return this.glFeatures.movedMrSidebar;
},
+ isNotificationsTodosButtons() {
+ return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
+ },
draftLabel() {
return this.draft ? this.$options.i18n.markAsReady : this.$options.i18n.markAsDraft;
},
@@ -250,7 +251,9 @@ export default {
/>
</div>
</template>
- <gl-disclosure-dropdown-group v-if="isLoggedIn && isMovedMrSidebar">
+ <gl-disclosure-dropdown-group
+ v-if="isLoggedIn && isMovedMrSidebar && !isNotificationsTodosButtons"
+ >
<sidebar-subscriptions-widget
:iid="String(mr.iid)"
:full-path="fullPath"
@@ -261,7 +264,10 @@ export default {
<gl-disclosure-dropdown-group
bordered
- :class="{ 'gl-mt-0! gl-pt-0! gl-border-t-0!': !(isLoggedIn && isMovedMrSidebar) }"
+ :class="{
+ 'gl-mt-0! gl-pt-0! gl-border-t-0!':
+ !(isLoggedIn && isMovedMrSidebar) || isNotificationsTodosButtons,
+ }"
>
<gl-disclosure-dropdown-item
v-if="canUpdateMergeRequest"
@@ -358,8 +364,6 @@ export default {
</gl-disclosure-dropdown-group>
</gl-disclosure-dropdown>
- <new-header-actions-popover v-if="isMovedMrSidebar" :issue-type="issuableType" />
-
<abuse-category-selector
v-if="!isCurrentUser && isReportAbuseDrawerOpen"
:reported-user-id="reportedUserId"
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index fac32bfdb24..cb9b85b9ef3 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -12,7 +12,7 @@ export default {
</script>
<template>
- <timeline-entry-item class="note note-wrapper" data-qa-selector="skeleton_note_placeholder">
+ <timeline-entry-item class="note note-wrapper">
<div
class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600"
></div>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue
index 36e608a068b..f59664e8d1d 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlCollapsibleListbox, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { REGISTRATION_TOKEN_PLACEHOLDER } from '../constants';
@@ -8,8 +8,7 @@ import getRunnerSetupInstructionsQuery from '../graphql/get_runner_setup.query.g
export default {
components: {
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
GlLoadingIcon,
ModalCopyButton,
},
@@ -27,7 +26,7 @@ export default {
},
data() {
return {
- selectedArchitecture: this.platform?.architectures[0] || null,
+ selectedArchName: this.platform?.architectures[0]?.name || null,
instructions: null,
};
},
@@ -55,6 +54,9 @@ export default {
architectures() {
return this.platform?.architectures || [];
},
+ selectedArchitecture() {
+ return this.architectures.find(({ name }) => name === this.selectedArchName) || null;
+ },
binaryUrl() {
return this.selectedArchitecture?.downloadLocation;
},
@@ -69,20 +71,22 @@ export default {
}
return registerInstructions;
},
+ listboxItems() {
+ return this.architectures.map(({ name }) => {
+ return { text: name, value: name };
+ });
+ },
},
watch: {
platform() {
// reset selection if architecture is not in this list
- const arch = this.architectures.find(({ name }) => name === this.selectedArchitecture.name);
+ const arch = this.architectures.find(({ name }) => name === this.selectedArchName);
if (!arch) {
- this.selectArchitecture(this.architectures[0]);
+ this.selectedArchName = this.architectures[0]?.name || null;
}
},
},
methods: {
- selectArchitecture(architecture) {
- this.selectedArchitecture = architecture;
- },
onClose() {
this.$emit('close');
},
@@ -104,18 +108,7 @@ export default {
<gl-loading-icon v-if="$apollo.loading" size="sm" inline />
</h5>
- <gl-dropdown class="gl-mb-3" :text="selectedArchitecture.name">
- <gl-dropdown-item
- v-for="architecture in architectures"
- :key="architecture.name"
- is-check-item
- :is-checked="selectedArchitecture.name === architecture.name"
- data-testid="architecture-dropdown-item"
- @click="selectArchitecture(architecture)"
- >
- {{ architecture.name }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-collapsible-listbox v-model="selectedArchName" class="gl-mb-3" :items="listboxItems" />
<div class="gl-sm-display-flex gl-align-items-center gl-mb-3">
<h5>{{ $options.i18n.downloadInstallBinary }}</h5>
<gl-button
diff --git a/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue b/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
index f50706b6de8..e0e8200580a 100644
--- a/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
+++ b/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
@@ -1,6 +1,21 @@
<script>
import { GlButtonGroup, GlButton } from '@gitlab/ui';
+const validateOptionsProp = (options) => {
+ const requiredOptionPropType = {
+ value: ['string', 'number', 'boolean'],
+ disabled: ['boolean', 'undefined'],
+ };
+ const optionProps = Object.keys(requiredOptionPropType);
+
+ return options.every((option) => {
+ if (!option) {
+ return false;
+ }
+ return optionProps.every((name) => requiredOptionPropType[name].includes(typeof option[name]));
+ });
+};
+
// TODO: We're planning to move this component to GitLab UI
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1787
export default {
@@ -12,6 +27,7 @@ export default {
options: {
type: Array,
required: true,
+ validator: validateOptionsProp,
},
value: {
type: [String, Number, Boolean],
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
new file mode 100644
index 00000000000..9bce9402afa
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import CommitInfo from '~/repository/components/commit_info.vue';
+import { calculateBlameOffset, toggleBlameClasses } from '../utils';
+
+export default {
+ name: 'BlameInfo',
+ components: {
+ CommitInfo,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
+ },
+ props: {
+ blameData: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ blameInfo() {
+ return this.blameData.map((blame, index) => ({
+ ...blame,
+ blameOffset: calculateBlameOffset(blame.lineno, index),
+ }));
+ },
+ },
+ mounted() {
+ toggleBlameClasses(this.blameData, true);
+ },
+ destroyed() {
+ toggleBlameClasses(this.blameData, false);
+ },
+};
+</script>
+<template>
+ <div class="blame gl-bg-gray-10">
+ <div class="blame-commit gl-border-none!">
+ <commit-info
+ v-for="(blame, index) in blameInfo"
+ :key="index"
+ :class="{ 'gl-border-t': index !== 0 }"
+ class="gl-display-flex gl-absolute gl-px-3"
+ :style="{ top: blame.blameOffset }"
+ :commit="blame.commit"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index 797a38d8171..4d5d877d43b 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -258,7 +258,7 @@ export default {
:class="$options.userColorScheme"
data-type="simple"
:data-path="blob.path"
- data-qa-selector="blob_viewer_file_content"
+ data-testid="blob-viewer-file-content"
>
<codeowners-validation
v-if="isCodeownersFile"
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
new file mode 100644
index 00000000000..af01653fc0d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
@@ -0,0 +1,37 @@
+const BLAME_INFO_CLASSLIST = ['gl-border-t', 'gl-border-gray-500', 'gl-pt-3!'];
+const PADDING_BOTTOM_LARGE = 'gl-pb-6!';
+const PADDING_BOTTOM_SMALL = 'gl-pb-3!';
+
+const findLineNumberElement = (lineNumber) => document.getElementById(`L${lineNumber}`);
+
+const findLineContentElement = (lineNumber) => document.getElementById(`LC${lineNumber}`);
+
+export const calculateBlameOffset = (lineNumber) => {
+ if (lineNumber === 1) return '0px';
+ const lineContentOffset = findLineContentElement(lineNumber)?.offsetTop;
+ return `${lineContentOffset}px`;
+};
+
+export const toggleBlameClasses = (blameData, isVisible) => {
+ /**
+ * Adds/removes classes to line number/content elements to match the line with the blame info
+ * */
+ const method = isVisible ? 'add' : 'remove';
+ blameData.forEach(({ lineno, span }) => {
+ const lineNumberEl = findLineNumberElement(lineno)?.parentElement;
+ const lineContentEl = findLineContentElement(lineno);
+ const lineNumberSpanEl = findLineNumberElement(lineno + span - 1)?.parentElement;
+ const lineContentSpanEl = findLineContentElement(lineno + span - 1);
+
+ lineNumberEl?.classList[method](...BLAME_INFO_CLASSLIST);
+ lineContentEl?.classList[method](...BLAME_INFO_CLASSLIST);
+
+ if (span === 1) {
+ lineNumberSpanEl?.classList[method](PADDING_BOTTOM_LARGE);
+ lineContentSpanEl?.classList[method](PADDING_BOTTOM_LARGE);
+ } else {
+ lineNumberSpanEl?.classList[method](PADDING_BOTTOM_SMALL);
+ lineContentSpanEl?.classList[method](PADDING_BOTTOM_SMALL);
+ }
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index 7c9a1bcd8cc..058a00e169a 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTruncate, GlTooltipDirective } from '@gitlab/ui';
import { DATE_TIME_FORMATS, DEFAULT_DATE_TIME_FORMAT } from '~/lib/utils/datetime_utility';
import timeagoMixin from '../mixins/timeago';
@@ -12,6 +12,9 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ components: {
+ GlTruncate,
+ },
mixins: [timeagoMixin],
props: {
time: {
@@ -34,11 +37,19 @@ export default {
default: DEFAULT_DATE_TIME_FORMAT,
validator: (timeFormat) => DATE_TIME_FORMATS.includes(timeFormat),
},
+ enableTruncation: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
timeAgo() {
return this.timeFormatted(this.time, this.dateTimeFormat);
},
+ tooltipText() {
+ return this.enableTruncation ? undefined : this.tooltipTitle(this.time);
+ },
},
};
</script>
@@ -46,8 +57,11 @@ export default {
<time
v-gl-tooltip.viewport="{ placement: tooltipPlacement }"
:class="cssClass"
- :title="tooltipTitle(time)"
+ :title="tooltipText"
:datetime="time"
- ><slot :time-ago="timeAgo">{{ timeAgo }}</slot></time
+ ><slot :time-ago="timeAgo"
+ ><template v-if="enableTruncation"><gl-truncate :text="timeAgo" with-tooltip /></template
+ ><template v-else>{{ timeAgo }}</template></slot
+ ></time
>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_labels.vue b/app/assets/javascripts/vue_shared/components/toggle_labels.vue
new file mode 100644
index 00000000000..05c837e32f0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/toggle_labels.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlToggle } from '@gitlab/ui';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
+import setIsShowingLabelsMutation from '~/graphql_shared/client/set_is_showing_labels.mutation.graphql';
+
+export default {
+ components: {
+ GlToggle,
+ LocalStorageSync,
+ },
+ data() {
+ return {
+ isShowingLabels: null,
+ };
+ },
+ apollo: {
+ isShowingLabels: {
+ query: isShowingLabelsQuery,
+ update: (data) => data.isShowingLabels,
+ },
+ },
+ computed: {
+ trackProperty() {
+ return this.isShowingLabels ? 'on' : 'off';
+ },
+ },
+ methods: {
+ setShowLabels(val) {
+ this.$apollo.mutate({
+ mutation: setIsShowingLabelsMutation,
+ variables: {
+ isShowingLabels: val,
+ },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="board-labels-toggle-wrapper gl-display-flex gl-align-items-center gl-ml-3 gl-h-7">
+ <local-storage-sync
+ :value="isShowingLabels"
+ storage-key="gl-show-board-labels"
+ @input="setShowLabels"
+ />
+ <gl-toggle
+ :value="isShowingLabels"
+ :label="__('Show labels')"
+ :data-track-property="trackProperty"
+ data-track-action="toggle"
+ data-track-label="show_labels"
+ label-position="left"
+ aria-describedby="board-labels-toggle-text"
+ data-testid="show-labels-toggle"
+ data-qa-selector="show_labels_toggle"
+ class="gl-flex-direction-row"
+ @change="setShowLabels"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
index 9665e188469..46496d2e483 100644
--- a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
+++ b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
@@ -2,12 +2,7 @@
export default {
provide() {
return {
- // We can't use this.vuexModule due to bug in vue-apollo when
- // provide is called in beforeCreate
- // See https://github.com/vuejs/vue-apollo/pull/1153 for details
-
- // @vue-compat does not care to normalize propsData fields
- vuexModule: this.$options.propsData.vuexModule ?? this.$options.propsData['vuex-module'],
+ vuexModule: this.vuexModule,
};
},
props: {
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 beb8321a271..9fb0add5522 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -355,7 +355,7 @@ export default {
<span data-testid="action-primary-text" class="gl-font-weight-bold gl-mb-2">{{
action.text
}}</span>
- <span data-testid="action-secondary-text" class="gl-text-gray-700">
+ <span data-testid="action-secondary-text" class="gl-font-sm gl-text-secondary">
{{ action.secondaryText }}
</span>
</div>
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 9c001fa2e9a..81e75c4e1d5 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -97,4 +97,7 @@ export const confidentialityInfoText = (workspaceType, issuableType) =>
export const EDITING_MODE_KEY = 'gl-markdown-editor-mode';
export const EDITING_MODE_MARKDOWN_FIELD = 'markdownField';
export const EDITING_MODE_CONTENT_EDITOR = 'contentEditor';
+
export const CLEAR_AUTOSAVE_ENTRY_EVENT = 'markdown_clear_autosave_entry';
+export const CONTENT_EDITOR_READY_EVENT = 'content_editor_ready';
+export const MARKDOWN_EDITOR_READY_EVENT = 'markdown_editor_ready';
diff --git a/app/assets/javascripts/vue_shared/global_search/constants.js b/app/assets/javascripts/vue_shared/global_search/constants.js
index 43110c0c9af..14ea0389bad 100644
--- a/app/assets/javascripts/vue_shared/global_search/constants.js
+++ b/app/assets/javascripts/vue_shared/global_search/constants.js
@@ -8,6 +8,7 @@ export const ALL_GITLAB = __('All GitLab');
export const SEARCH_GITLAB = s__('GlobalSearch|Search GitLab');
export const PLACES = s__('GlobalSearch|Places');
+export const COMMAND_PALETTE = s__('GlobalSearch|Command palette');
export const SEARCH_DESCRIBED_BY_DEFAULT = s__(
'GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list.',
);
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
index 033bb8c3885..679332163b5 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_create_root.vue
@@ -22,6 +22,10 @@ export default {
type: String,
required: true,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
},
};
</script>
@@ -34,6 +38,7 @@ export default {
:description-help-path="descriptionHelpPath"
:labels-fetch-path="labelsFetchPath"
:labels-manage-path="labelsManagePath"
+ :issuable-type="issuableType"
>
<template #actions="issuableMeta">
<slot name="actions" v-bind="issuableMeta"></slot>
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
index 1cfa3f6d3d7..64f0ec3fbc7 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
@@ -1,15 +1,17 @@
<script>
-import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlFormCheckbox, GlFormGroup } from '@gitlab/ui';
import LabelsSelect from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
import { VARIANT_EMBEDDED } from '~/sidebar/components/labels/labels_select_widget/constants';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
+import { issuableTypeText } from '~/issues/constants';
export default {
VARIANT_EMBEDDED,
components: {
GlForm,
GlFormInput,
+ GlFormCheckbox,
GlFormGroup,
MarkdownEditor,
LabelsSelect,
@@ -31,6 +33,10 @@ export default {
type: String,
required: true,
},
+ issuableType: {
+ type: String,
+ required: true,
+ },
},
descriptionFormFieldProps: {
ariaLabel: __('Description'),
@@ -44,10 +50,20 @@ export default {
return {
issuableTitle: '',
issuableDescription: '',
+ issuableConfidential: false,
selectedLabels: [],
};
},
- computed: {},
+ computed: {
+ confidentialityText() {
+ return sprintf(
+ __(
+ 'This %{issuableType} is confidential and should only be visible to team members with at least Reporter access.',
+ ),
+ { issuableType: issuableTypeText[this.issuableType] },
+ );
+ },
+ },
methods: {
handleUpdateSelectedLabels(labels) {
if (labels.length) {
@@ -85,6 +101,15 @@ export default {
/>
</div>
</div>
+ <div data-testid="issuable-confidential" class="form-group row">
+ <div class="col-12">
+ <gl-form-group :label="__('Confidentiality')" label-for="issuable-confidential">
+ <gl-form-checkbox id="issuable-confidential" v-model="issuableConfidential">
+ {{ confidentialityText }}
+ </gl-form-checkbox>
+ </gl-form-group>
+ </div>
+ </div>
<div data-testid="issuable-labels" class="form-group row">
<label for="issuable-labels" class="col-12">{{ __('Labels') }}</label>
<div class="col-12">
@@ -111,6 +136,7 @@ export default {
name="actions"
:issuable-title="issuableTitle"
:issuable-description="issuableDescription"
+ :issuable-confidential="issuableConfidential"
:selected-labels="selectedLabels"
></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index 690d9523a63..bb36df0a778 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -8,7 +8,6 @@ import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
import IssuableAssignees from '~/issuable/components/issue_assignees.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import SafeHtml from '~/vue_shared/directives/safe_html';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import { STATE_CLOSED } from '~/work_items/constants';
import { isAssigneesWidget, isLabelsWidget } from '~/work_items/utils';
@@ -25,7 +24,6 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml,
},
mixins: [timeagoMixin],
props: {
@@ -91,9 +89,6 @@ export default {
authorId() {
return getIdFromGraphQLId(this.author.id);
},
- isIssueTrackerExternal() {
- return Boolean(this.issuable.externalTracker);
- },
isIssuableUrlExternal() {
return isExternal(this.webUrl ?? '');
},
@@ -266,36 +261,20 @@ export default {
v-if="issuable.hidden"
v-gl-tooltip
name="spam"
- :title="__('This issue is hidden because its author has been banned')"
+ :title="__('This issue is hidden because its author has been banned.')"
:aria-label="__('Hidden')"
/>
- <template v-if="isIssueTrackerExternal">
- <gl-link
- class="issue-title-text"
- dir="auto"
- :href="webUrl"
- data-qa-selector="issuable_title_link"
- data-testid="issuable-title-link"
- v-bind="issuableTitleProps"
- @click="handleIssuableItemClick"
- >
- {{ issuable.title }}
- <gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
- </gl-link>
- </template>
- <template v-else>
- <gl-link
- v-safe-html="issuable.titleHtml || issuable.title"
- class="issue-title-text"
- dir="auto"
- :href="webUrl"
- data-qa-selector="issuable_title_link"
- data-testid="issuable-title-link"
- v-bind="issuableTitleProps"
- @click="handleIssuableItemClick"
- />
+ <gl-link
+ class="issue-title-text"
+ dir="auto"
+ :href="webUrl"
+ data-testid="issuable-title-link"
+ v-bind="issuableTitleProps"
+ @click="handleIssuableItemClick"
+ >
+ {{ issuable.title }}
<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
- </template>
+ </gl-link>
<span
v-if="taskStatus"
class="task-status gl-display-none gl-sm-display-inline-block! gl-ml-2 gl-font-sm"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
index c4b92454ac0..a9b5e3a66a8 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
@@ -1,6 +1,8 @@
<script>
import { GlIcon, GlBadge, GlButton, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import HiddenBadge from '~/issuable/components/hidden_badge.vue';
+import LockedBadge from '~/issuable/components/locked_badge.vue';
import { issuableStatusText, STATUS_OPEN, STATUS_REOPENED } from '~/issues/constants';
import { isExternal } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
@@ -16,6 +18,8 @@ export default {
GlButton,
GlLink,
GlSprintf,
+ HiddenBadge,
+ LockedBadge,
TimeAgoTooltip,
WorkItemTypeIcon,
},
@@ -101,16 +105,6 @@ export default {
? 'success'
: 'info';
},
- blockedTooltip() {
- return sprintf(__('This %{issuable} is locked. Only project members can comment.'), {
- issuable: this.issuableType,
- });
- },
- hiddenTooltip() {
- return sprintf(__('This %{issuable} is hidden because its author has been banned'), {
- issuable: this.issuableType,
- });
- },
shouldShowWorkItemTypeIcon() {
return this.showWorkItemTypeIcon && this.issuableType;
},
@@ -174,22 +168,8 @@ export default {
:issuable-type="issuableType"
:workspace-type="workspaceType"
/>
- <span v-if="blocked" class="issuable-warning-icon">
- <gl-icon
- v-gl-tooltip.bottom
- name="lock"
- :title="blockedTooltip"
- :aria-label="__('Blocked')"
- />
- </span>
- <span v-if="isHidden" class="issuable-warning-icon">
- <gl-icon
- v-gl-tooltip.bottom
- name="spam"
- :title="hiddenTooltip"
- :aria-label="__('Hidden')"
- />
- </span>
+ <locked-badge v-if="blocked" :issuable-type="issuableType" />
+ <hidden-badge v-if="isHidden" :issuable-type="issuableType" />
<work-item-type-icon
v-if="shouldShowWorkItemTypeIcon"
show-text
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index f54c4c52743..3412848a9b7 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -138,7 +138,10 @@ export default {
</div>
<template v-if="activePanel">
- <div class="gl-display-flex gl-align-items-center gl-py-5">
+ <div
+ data-testid="active-panel-template"
+ class="gl-display-flex gl-align-items-center gl-py-5"
+ >
<div class="col-auto">
<img aria-hidden :src="activePanel.imageSrc" />
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index 57faed61280..c867e53dc30 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -5,6 +5,7 @@ import { ASC } from '~/notes/constants';
import { __ } from '~/locale';
import { clearDraft } from '~/lib/utils/autosave';
import createNoteMutation from '../../graphql/notes/create_work_item_note.mutation.graphql';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import { TRACKING_CATEGORY_SHOW, i18n } from '../../constants';
import WorkItemNoteSignedOut from './work_item_note_signed_out.vue';
@@ -21,8 +22,12 @@ export default {
WorkItemCommentForm,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -90,7 +95,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
@@ -109,6 +116,9 @@ export default {
},
},
computed: {
+ isLoading() {
+ return this.$apollo.queries.workItem.loading;
+ },
signedIn() {
return Boolean(window.gon.current_user_id);
},
@@ -248,7 +258,7 @@ export default {
<li :class="timelineEntryClass">
<work-item-note-signed-out v-if="!signedIn" />
<work-item-comment-locked
- v-else-if="!canCreateNote"
+ v-else-if="!isLoading && !canCreateNote"
:work-item-type="workItemType"
:is-project-archived="isProjectArchived"
/>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index a79169bde1e..c7d8a50f402 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -35,7 +35,6 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
props: {
workItemId: {
type: String,
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
index fd8842aa01a..fed21a1c277 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
@@ -18,8 +18,11 @@ export default {
DiscussionNotesRepliesWrapper,
WorkItemNoteReplying,
},
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -154,6 +157,7 @@ export default {
:is-first-note="true"
:note="note"
:discussion-id="discussionId"
+ :full-path="fullPath"
:has-replies="hasReplies"
:work-item-type="workItemType"
:is-modal="isModal"
@@ -180,6 +184,7 @@ export default {
:is-first-note="true"
:note="note"
:discussion-id="discussionId"
+ :full-path="fullPath"
:has-replies="hasReplies"
:work-item-type="workItemType"
:is-modal="isModal"
@@ -207,6 +212,7 @@ export default {
<work-item-note
:key="threadKey(reply)"
:discussion-id="discussionId"
+ :full-path="fullPath"
:note="reply"
:work-item-type="workItemType"
:is-modal="isModal"
@@ -231,6 +237,7 @@ export default {
v-if="shouldShowReplyForm"
:notes-form="false"
:autofocus="autofocus"
+ :full-path="fullPath"
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:discussion-id="discussionId"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index b5e3ea68725..f4c654f054c 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -3,7 +3,6 @@ import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
-import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import Tracking from '~/tracking';
import { updateDraft, clearDraft } from '~/lib/utils/autosave';
import { renderMarkdown } from '~/notes/utils';
@@ -11,15 +10,17 @@ import { getLocationHash } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import EditedAt from '~/issues/show/components/edited.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
-import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
-import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { i18n, TRACKING_CATEGORY_SHOW } from '../../constants';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
+import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import updateWorkItemNoteMutation from '../../graphql/notes/update_work_item_note.mutation.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import { isAssigneesWidget } from '../../utils';
import WorkItemCommentForm from './work_item_comment_form.vue';
+import NoteActions from './work_item_note_actions.vue';
import WorkItemNoteAwardsList from './work_item_note_awards_list.vue';
+import NoteBody from './work_item_note_body.vue';
export default {
name: 'WorkItemNoteThread',
@@ -35,8 +36,12 @@ export default {
EditedAt,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -169,7 +174,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
@@ -335,6 +342,7 @@ export default {
</note-header>
<div class="gl-display-inline-flex">
<note-actions
+ :full-path="fullPath"
:show-award-emoji="hasAwardEmojiPermission"
:work-item-iid="workItemIid"
:note="note"
@@ -372,7 +380,12 @@ export default {
/>
</div>
<div class="note-awards" :class="isFirstNote ? '' : 'gl-pl-7'">
- <work-item-note-awards-list :note="note" :work-item-iid="workItemIid" :is-modal="isModal" />
+ <work-item-note-awards-list
+ :full-path="fullPath"
+ :note="note"
+ :work-item-iid="workItemIid"
+ :is-modal="isModal"
+ />
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
index e5da3d346ae..2cdf8b5ea9d 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
@@ -33,8 +33,11 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemIid: {
type: String,
required: true,
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
index 3c30c204ab6..17d22e66530 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
@@ -8,8 +8,11 @@ export default {
components: {
AwardsList,
},
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemIid: {
type: String,
required: true,
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
index f50cfac90f7..49813edf6fc 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -43,9 +43,14 @@ export default {
type: Boolean,
required: true,
},
- childPath: {
- type: String,
- required: true,
+ /*
+ This flag is added to manage between two different work items; Task and Objective/Key result.
+ Status icon is shown on the task while the actual task icon is shown on any Objective/Key result.
+ */
+ showTaskIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
},
},
computed: {
@@ -69,7 +74,7 @@ export default {
return this.childItem.state === STATE_OPEN;
},
iconName() {
- if (this.childItemType === TASK_TYPE_NAME) {
+ if (this.childItemType === TASK_TYPE_NAME && !this.showTaskIcon) {
return this.isChildItemOpen ? 'issue-open-m' : 'issue-close';
}
return WORK_ITEM_NAME_TO_ICON_MAP[this.childItemType];
@@ -78,7 +83,7 @@ export default {
return this.childItem.workItemType.name;
},
iconClass() {
- if (this.childItemType === TASK_TYPE_NAME) {
+ if (this.childItemType === TASK_TYPE_NAME && !this.showTaskIcon) {
return this.isChildItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
}
return '';
@@ -148,9 +153,8 @@ export default {
/>
</span>
<gl-link
- :href="childPath"
- class="gl-text-truncate gl-font-weight-semibold"
- data-testid="item-title"
+ :href="childItem.webUrl"
+ class="gl-overflow-break-word gl-font-weight-semibold"
@click="$emit('click', $event)"
@mouseover="$emit('mouseover')"
@mouseout="$emit('mouseout')"
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
index 38d8d239a7e..c0e87f0bb6e 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
@@ -69,6 +69,7 @@ export default {
badge-tooltip-prop="name"
:badge-sr-only-text="assigneesCollapsedTooltip"
:class="assigneesContainerClass"
+ class="gl-white-space-nowrap"
>
<template #avatar="{ avatar }">
<gl-avatar-link v-gl-tooltip target="blank" :href="avatar.webUrl" :title="avatar.name">
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
index 7b38e838033..3595ab631df 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
@@ -7,7 +7,6 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import {
WORK_ITEMS_TYPE_MAP,
- WORK_ITEM_TYPE_ENUM_TASK,
I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
sprintfWorkItem,
} from '../../constants';
@@ -29,7 +28,7 @@ export default {
childrenType: {
type: String,
required: false,
- default: WORK_ITEM_TYPE_ENUM_TASK,
+ default: '',
},
childrenIds: {
type: Array,
@@ -53,7 +52,7 @@ export default {
return {
fullPath: this.fullPath,
searchTerm: this.search?.title || this.search,
- types: [this.childrenType],
+ types: this.childrenType ? [this.childrenType] : [],
in: this.search ? 'TITLE' : undefined,
};
},
@@ -106,6 +105,7 @@ export default {
},
handleFocus() {
this.searchStarted = true;
+ this.$emit('searching', true);
},
handleMouseOver() {
this.timeout = setTimeout(() => {
@@ -115,11 +115,22 @@ export default {
handleMouseOut() {
clearTimeout(this.timeout);
},
+ handleBlur() {
+ this.$emit('searching', false);
+ },
+ focusInputText() {
+ this.$nextTick(() => {
+ if (this.areWorkItemsToAddValid) {
+ this.$refs.tokenSelector.$el.querySelector('input[type="text"]').focus();
+ }
+ });
+ },
},
};
</script>
<template>
<gl-token-selector
+ ref="tokenSelector"
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
@@ -131,13 +142,14 @@ export default {
@focus="handleFocus"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
+ @token-add="focusInputText"
+ @token-remove="focusInputText"
+ @blur="handleBlur"
>
- <template #token-content="{ token }">
- {{ token.title }}
- </template>
+ <template #token-content="{ token }"> {{ token.iid }} {{ token.title }} </template>
<template #dropdown-item-content="{ dropdownItem }">
<div class="gl-display-flex">
- <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
+ <div class="gl-text-secondary gl-font-sm gl-mr-4">{{ dropdownItem.iid }}</div>
<div class="gl-text-truncate">{{ dropdownItem.title }}</div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 18aa4d55086..02d2ea24ca0 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -7,7 +7,6 @@ import {
GlModalDirective,
GlToggle,
} from '@gitlab/ui';
-import { produce } from 'immer';
import * as Sentry from '@sentry/browser';
@@ -15,7 +14,6 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import toast from '~/vue_shared/plugins/global_toast';
import { isLoggedIn } from '~/lib/utils/common_utils';
-import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
sprintfWorkItem,
@@ -28,7 +26,6 @@ import {
TEST_ID_PROMOTE_ACTION,
TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
TEST_ID_COPY_REFERENCE_ACTION,
- WIDGET_TYPE_NOTIFICATIONS,
I18N_WORK_ITEM_ERROR_CONVERTING,
WORK_ITEM_TYPE_VALUE_KEY_RESULT,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
@@ -70,8 +67,12 @@ export default {
copyCreateNoteEmailTestId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
deleteActionTestId: TEST_ID_DELETE_ACTION,
promoteActionTestId: TEST_ID_PROMOTE_ACTION,
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: false,
@@ -127,10 +128,6 @@ export default {
required: false,
default: false,
},
- workItemIid: {
- type: String,
- required: true,
- },
},
apollo: {
workItemTypes: {
@@ -199,80 +196,31 @@ export default {
}
},
toggleNotifications(subscribed) {
- const inputVariables = {
- projectPath: this.fullPath,
- iid: this.workItemIid,
- subscribedState: subscribed,
- };
this.$apollo
.mutate({
mutation: updateWorkItemNotificationsMutation,
variables: {
- input: inputVariables,
- },
- optimisticResponse: {
- updateWorkItemNotificationsSubscription: {
- issue: {
- id: this.workItemId,
- subscribed,
- },
- errors: [],
- },
- },
- update: (
- cache,
- {
- data: {
- updateWorkItemNotificationsSubscription: { issue = {} },
- },
+ input: {
+ id: this.workItemId,
+ subscribed,
},
- ) => {
- // As the mutation and the query both are different,
- // overwrite the subscribed value in the cache
- this.updateWorkItemNotificationsWidgetCache({
- cache,
- issue,
- });
},
})
- .then(
- ({
- data: {
- updateWorkItemNotificationsSubscription: { errors },
- },
- }) => {
- if (errors?.length) {
- throw new Error(errors[0]);
- }
- toast(
- subscribed ? this.$options.i18n.notificationOn : this.$options.i18n.notificationOff,
- );
- },
- )
+ .then(({ data }) => {
+ const { errors } = data.workItemSubscribe;
+ if (errors?.length) {
+ throw new Error(errors[0]);
+ }
+
+ toast(
+ subscribed ? this.$options.i18n.notificationOn : this.$options.i18n.notificationOff,
+ );
+ })
.catch((error) => {
this.$emit('error', error.message);
Sentry.captureException(error);
});
},
- updateWorkItemNotificationsWidgetCache({ cache, issue }) {
- const query = {
- query: workItemByIidQuery,
- variables: { fullPath: this.fullPath, iid: this.workItemIid },
- };
- // Read the work item object
- const sourceData = cache.readQuery(query);
-
- const newData = produce(sourceData, (draftState) => {
- const { widgets } = draftState.workspace.workItems.nodes[0];
-
- const widgetNotifications = widgets.find(({ type }) => type === WIDGET_TYPE_NOTIFICATIONS);
- // overwrite the subscribed value
- widgetNotifications.subscribed = issue.subscribed;
- });
-
- // write to the cache
- cache.writeQuery({ ...query, data: newData });
- },
throwConvertError() {
this.$emit('error', this.i18n.convertError);
},
@@ -337,7 +285,6 @@ export default {
:data-testid="$options.notificationsToggleTestId"
class="work-item-notification-toggle"
label-position="left"
- label-id="notifications-toggle"
@change="toggleNotifications($event)"
/>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index f9527884adc..a9aafbb3d84 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -13,7 +13,8 @@ import {
import { debounce, uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
-import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
+import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql';
+import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { n__, s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -54,8 +55,12 @@ export default {
GlIntersectionObserver,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -99,7 +104,7 @@ export default {
apollo: {
users: {
query() {
- return userSearchQuery;
+ return this.isGroup ? groupUsersSearchQuery : usersSearchQuery;
},
variables() {
return {
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index 139f0f7919c..fd01d855782 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -4,17 +4,21 @@ import {
sprintfWorkItem,
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_HEALTH_STATUS,
+ WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_ITERATION,
WIDGET_TYPE_LABELS,
WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_PROGRESS,
WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_WEIGHT,
+ WORK_ITEM_TYPE_VALUE_KEY_RESULT,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
} from '../constants';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
import WorkItemMilestone from './work_item_milestone.vue';
+import WorkItemParent from './work_item_parent.vue';
export default {
components: {
@@ -22,6 +26,7 @@ export default {
WorkItemMilestone,
WorkItemAssignees,
WorkItemDueDate,
+ WorkItemParent,
WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
@@ -29,8 +34,11 @@ export default {
import('ee_component/work_items/components/work_item_health_status.vue'),
},
mixins: [glFeatureFlagMixin()],
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItem: {
type: Object,
required: true,
@@ -81,9 +89,21 @@ export default {
workItemHealthStatus() {
return this.isWidgetPresent(WIDGET_TYPE_HEALTH_STATUS);
},
+ workItemHierarchy() {
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
+ },
workItemMilestone() {
return this.isWidgetPresent(WIDGET_TYPE_MILESTONE);
},
+ showWorkItemParent() {
+ return (
+ this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE ||
+ this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT
+ );
+ },
+ workItemParent() {
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
+ },
},
methods: {
isWidgetPresent(type) {
@@ -98,6 +118,7 @@ export default {
<work-item-assignees
v-if="workItemAssignees"
:can-update="canUpdate"
+ :full-path="fullPath"
:work-item-id="workItem.id"
:assignees="workItemAssignees.assignees.nodes"
:allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
@@ -108,6 +129,7 @@ export default {
<work-item-labels
v-if="workItemLabels"
:can-update="canUpdate"
+ :full-path="fullPath"
:work-item-id="workItem.id"
:work-item-iid="workItem.iid"
@error="$emit('error', $event)"
@@ -123,6 +145,7 @@ export default {
/>
<work-item-milestone
v-if="workItemMilestone"
+ :full-path="fullPath"
:work-item-id="workItem.id"
:work-item-milestone="workItemMilestone.milestone"
:work-item-type="workItemType"
@@ -151,6 +174,7 @@ export default {
<work-item-iteration
v-if="workItemIteration"
class="gl-mb-5"
+ :full-path="fullPath"
:iteration="workItemIteration.iteration"
:can-update="canUpdate"
:work-item-id="workItem.id"
@@ -168,5 +192,14 @@ export default {
:work-item-type="workItemType"
@error="$emit('error', $event)"
/>
+ <work-item-parent
+ v-if="showWorkItemParent"
+ class="gl-mb-5"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ :parent="workItemParent"
+ @error="$emit('error', $event)"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
index 14e55134048..460b5d35187 100644
--- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue
+++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue
@@ -3,10 +3,11 @@ import { GlAvatarLink, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { WORKSPACE_PROJECT } from '~/issues/constants';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
+import WorkItemStateBadge from './work_item_state_badge.vue';
+import WorkItemTypeIcon from './work_item_type_icon.vue';
export default {
components: {
@@ -18,8 +19,12 @@ export default {
ConfidentialityBadge,
GlLoadingIcon,
},
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemIid: {
type: String,
required: false,
@@ -59,7 +64,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 58bf524f450..b7f3ac93cdb 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -10,6 +10,7 @@ import Tracking from '~/tracking';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { autocompleteDataSources, markdownPreviewPath } from '../utils';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
import WorkItemDescriptionRendered from './work_item_description_rendered.vue';
@@ -25,8 +26,12 @@ export default {
WorkItemDescriptionRendered,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -55,7 +60,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index edecd7addcc..53929775684 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -16,7 +16,6 @@ import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
-import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import { WORKSPACE_PROJECT } from '~/issues/constants';
@@ -37,6 +36,7 @@ import {
import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import { findHierarchyWidgetChildren } from '../utils';
@@ -52,6 +52,7 @@ import WorkItemDetailModal from './work_item_detail_modal.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
import WorkItemStateToggleButton from './work_item_state_toggle_button.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
+import WorkItemTypeIcon from './work_item_type_icon.vue';
export default {
i18n,
@@ -84,7 +85,7 @@ export default {
WorkItemRelationships,
},
mixins: [glFeatureFlagMixin()],
- inject: ['fullPath', 'reportAbusePath'],
+ inject: ['fullPath', 'isGroup', 'reportAbusePath'],
props: {
isModal: {
type: Boolean,
@@ -118,7 +119,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
@@ -189,8 +192,8 @@ export default {
canAssignUnassignUser() {
return this.workItemAssignees && this.canSetWorkItemMetadata;
},
- fullPath() {
- return this.workItem?.project.fullPath;
+ projectFullPath() {
+ return this.workItem?.project?.fullPath;
},
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
@@ -460,11 +463,12 @@ export default {
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
:work-item-iid="workItemIid"
- :work-item-fullpath="workItem.project.fullPath"
+ :work-item-fullpath="projectFullPath"
:current-user-todos="currentUserTodos"
@error="updateError = $event"
/>
<work-item-actions
+ :full-path="fullPath"
:work-item-id="workItem.id"
:subscribed-to-notifications="workItemNotificationsSubscribed"
:work-item-type="workItemType"
@@ -476,7 +480,6 @@ export default {
:work-item-reference="workItem.reference"
:work-item-create-note-email="workItem.createNoteEmail"
:is-modal="isModal"
- :work-item-iid="workItemIid"
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"
@@ -503,6 +506,7 @@ export default {
@error="updateError = $event"
/>
<work-item-created-updated
+ :full-path="fullPath"
:work-item-iid="workItemIid"
:update-in-progress="updateInProgress"
/>
@@ -535,11 +539,12 @@ export default {
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
:work-item-iid="workItemIid"
- :work-item-fullpath="workItem.project.fullPath"
+ :work-item-fullpath="projectFullPath"
:current-user-todos="currentUserTodos"
@error="updateError = $event"
/>
<work-item-actions
+ :full-path="fullPath"
:work-item-id="workItem.id"
:subscribed-to-notifications="workItemNotificationsSubscribed"
:work-item-type="workItemType"
@@ -551,7 +556,6 @@ export default {
:work-item-reference="workItem.reference"
:work-item-create-note-email="workItem.createNoteEmail"
:is-modal="isModal"
- :work-item-iid="workItemIid"
@deleteWorkItem="
$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })
"
@@ -571,12 +575,14 @@ export default {
<work-item-attributes-wrapper
:class="{ 'gl-md-display-none!': workItemsMvc2Enabled }"
class="gl-border-b"
+ :full-path="fullPath"
:work-item="workItem"
:work-item-parent-id="workItemParentId"
@error="updateError = $event"
/>
<work-item-description
v-if="hasDescriptionWidget"
+ :full-path="fullPath"
:work-item-id="workItem.id"
:work-item-iid="workItem.iid"
class="gl-pt-5"
@@ -585,7 +591,7 @@ export default {
<work-item-award-emoji
v-if="workItemAwardEmoji"
:work-item-id="workItem.id"
- :work-item-fullpath="workItem.project.fullPath"
+ :work-item-fullpath="projectFullPath"
:award-emoji="workItemAwardEmoji.awardEmoji"
:work-item-iid="workItemIid"
@error="updateError = $event"
@@ -593,6 +599,7 @@ export default {
/>
<work-item-tree
v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE"
+ :full-path="fullPath"
:work-item-type="workItemType"
:parent-work-item-type="workItem.workItemType.name"
:work-item-id="workItem.id"
@@ -605,12 +612,15 @@ export default {
/>
<work-item-relationships
v-if="showWorkItemLinkedItems"
+ :work-item-id="workItem.id"
:work-item-iid="workItemIid"
- :work-item-full-path="workItem.project.fullPath"
+ :work-item-full-path="projectFullPath"
+ :work-item-type="workItem.workItemType.name"
@showModal="openInModal"
/>
<work-item-notes
v-if="workItemNotes"
+ :full-path="fullPath"
:work-item-id="workItem.id"
:work-item-iid="workItem.iid"
:work-item-type="workItemType"
@@ -629,6 +639,7 @@ export default {
:title="$options.i18n.fetchErrorTitle"
:description="error"
:svg-path="noAccessSvgPath"
+ :svg-height="null"
/>
</section>
<aside
@@ -638,6 +649,7 @@ export default {
:class="{ 'is-modal': isModal }"
>
<work-item-attributes-wrapper
+ :full-path="fullPath"
:work-item="workItem"
:work-item-parent-id="workItemParentId"
@error="updateError = $event"
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index 1405a12a101..3cdbf816421 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -8,6 +8,7 @@ import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_it
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS, TRACKING_CATEGORY_SHOW } from '../constants';
import { isLabelsWidget } from '../utils';
@@ -37,8 +38,12 @@ export default {
LabelItem,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -65,7 +70,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
index 9d9414b5399..f4de7c1dddc 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
@@ -13,6 +13,7 @@ import { findHierarchyWidgets } from '../../utils';
import { addHierarchyChild, removeHierarchyChild } from '../../graphql/cache_utils';
import reorderWorkItem from '../../graphql/reorder_work_item.mutation.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import WorkItemLinkChild from './work_item_link_child.vue';
@@ -20,8 +21,12 @@ export default {
components: {
WorkItemLinkChild,
},
- inject: ['fullPath'],
+ inject: ['isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemType: {
type: String,
required: false,
@@ -83,7 +88,14 @@ export default {
const { data } = await this.$apollo.mutate({
mutation: updateWorkItemMutation,
variables: { input: { id: child.id, hierarchyWidget: { parentId: null } } },
- update: (cache) => removeHierarchyChild(cache, this.fullPath, this.workItemIid, child),
+ update: (cache) =>
+ removeHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ isGroup: this.isGroup,
+ workItem: child,
+ }),
});
if (data.workItemUpdate.errors.length) {
@@ -109,7 +121,14 @@ export default {
const { data } = await this.$apollo.mutate({
mutation: updateWorkItemMutation,
variables: { input: { id: child.id, hierarchyWidget: { parentId: this.workItemId } } },
- update: (cache) => addHierarchyChild(cache, this.fullPath, this.workItemIid, child),
+ update: (cache) =>
+ addHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ isGroup: this.isGroup,
+ workItem: child,
+ }),
});
if (data.workItemUpdate.errors.length) {
@@ -124,7 +143,7 @@ export default {
},
addWorkItemQuery({ iid }) {
this.$apollo.addSmartQuery('prefetchedWorkItem', {
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: {
fullPath: this.fullPath,
iid,
@@ -206,7 +225,7 @@ export default {
update: (store) => {
store.updateQuery(
{
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.fullPath, iid: this.workItemIid },
},
(sourceData) =>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 679287338c8..847a3585ac4 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -13,7 +13,6 @@ import {
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_NAME_TO_ICON_MAP,
} from '../../constants';
-import { workItemPath } from '../../utils';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
import WorkItemTreeChildren from './work_item_tree_children.vue';
@@ -27,7 +26,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['fullPath'],
props: {
canUpdate: {
type: Boolean,
@@ -90,9 +88,6 @@ export default {
stateTimestampTypeText() {
return this.isItemOpen ? __('Created') : __('Closed');
},
- childPath() {
- return workItemPath(this.fullPath, this.childItem.iid);
- },
chevronType() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
},
@@ -236,7 +231,6 @@ export default {
:can-update="canUpdate"
:parent-work-item-id="issuableGid"
:work-item-type="workItemType"
- :child-path="childPath"
@click="$emit('click', $event)"
@removeChild="$emit('removeChild', childItem)"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index eb836007e75..7fa6ac2c57f 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -18,6 +18,7 @@ import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_sel
import { FORM_TYPES, WIDGET_ICONS, WORK_ITEM_STATUS_TEXT } from '../../constants';
import { findHierarchyWidgetChildren } from '../../utils';
import { removeHierarchyChild } from '../../graphql/cache_utils';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemDetailModal from '../work_item_detail_modal.vue';
@@ -39,7 +40,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['fullPath', 'reportAbusePath'],
+ inject: ['fullPath', 'isGroup', 'reportAbusePath'],
props: {
issuableId: {
type: Number,
@@ -52,7 +53,9 @@ export default {
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
@@ -171,7 +174,13 @@ export default {
},
handleWorkItemDeleted(child) {
const { defaultClient: cache } = this.$apollo.provider.clients;
- removeHierarchyChild(cache, this.fullPath, this.iid, child);
+ removeHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.iid,
+ isGroup: this.isGroup,
+ workItem: child,
+ });
this.$toast.show(s__('WorkItem|Task deleted'));
},
updateWorkItemIdUrlQuery({ iid } = {}) {
@@ -256,6 +265,7 @@ export default {
v-if="isShownAddForm"
ref="wiLinksForm"
data-testid="add-links-form"
+ :full-path="fullPath"
:issuable-gid="issuableGid"
:work-item-iid="iid"
:children-ids="childrenIds"
@@ -269,6 +279,7 @@ export default {
<work-item-children-wrapper
:children="children"
:can-update="canUpdate"
+ :full-path="fullPath"
:work-item-id="issuableGid"
:work-item-iid="iid"
@error="error = $event"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index 55440e1603c..f24b56cac36 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -37,8 +37,12 @@ export default {
GlTooltip,
WorkItemTokenInput,
},
- inject: ['fullPath', 'hasIterationsFeature'],
+ inject: ['hasIterationsFeature', 'isGroup'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
issuableGid: {
type: String,
required: false,
@@ -225,7 +229,6 @@ export default {
this.error = null;
},
addChild() {
- this.searchStarted = false;
this.$apollo
.mutate({
mutation: updateWorkItemMutation,
@@ -261,7 +264,13 @@ export default {
input: this.workItemInput,
},
update: (cache, { data }) =>
- addHierarchyChild(cache, this.fullPath, this.workItemIid, data.workItemCreate.workItem),
+ addHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.workItemIid,
+ isGroup: this.isGroup,
+ workItem: data.workItemCreate.workItem,
+ }),
})
.then(({ data }) => {
if (data.workItemCreate?.errors?.length) {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
index bc3f5201fb8..b61b3b2e0d3 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -22,8 +22,11 @@ export default {
WorkItemLinksForm,
WorkItemChildrenWrapper,
},
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemType: {
type: String,
required: true,
@@ -139,6 +142,7 @@ export default {
v-if="isShownAddForm"
ref="wiLinksForm"
data-testid="add-tree-form"
+ :full-path="fullPath"
:issuable-gid="workItemId"
:work-item-iid="workItemIid"
:form-type="formType"
@@ -152,6 +156,7 @@ export default {
<work-item-children-wrapper
:children="children"
:can-update="canUpdate"
+ :full-path="fullPath"
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
index 2cabf489bc6..401223c3593 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
@@ -3,7 +3,6 @@ export default {
components: {
WorkItemLinkChild: () => import('./work_item_link_child.vue'),
},
- inject: ['fullPath'],
props: {
workItemType: {
type: String,
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
index 6cc61ed4756..a2cbb7f7598 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -46,8 +46,11 @@ export default {
GlDropdownText,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index 256f8ed53d1..fe8aea99f53 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -46,8 +46,11 @@ export default {
WorkItemNotesActivityHeader,
WorkItemHistoryOnlyFilterNote,
},
- inject: ['fullPath'],
props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: true,
@@ -364,6 +367,7 @@ export default {
<work-item-discussion
:key="getDiscussionKey(discussion)"
:discussion="discussion.notes.nodes"
+ :full-path="fullPath"
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue
new file mode 100644
index 00000000000..e16299f482f
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_parent.vue
@@ -0,0 +1,249 @@
+<script>
+import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { debounce } from 'lodash';
+
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { s__ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+
+import projectWorkItemsQuery from '../graphql/project_work_items.query.graphql';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+} from '../constants';
+
+export default {
+ i18n: {
+ assignParentLabel: s__('WorkItem|Assign parent'),
+ parentLabel: s__('WorkItem|Parent'),
+ none: s__('WorkItem|None'),
+ noMatchingResults: s__('WorkItem|No matching results'),
+ unAssign: s__('WorkItem|Unassign'),
+ workItemsFetchError: s__(
+ 'WorkItem|Something went wrong while fetching items. Please try again.',
+ ),
+ },
+ components: {
+ GlFormGroup,
+ GlCollapsibleListbox,
+ },
+ mixins: [glFeatureFlagMixin()],
+ inject: ['fullPath'],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ parent: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ updateInProgress: false,
+ searchStarted: false,
+ availableWorkItems: [],
+ localSelectedItem: this.parent?.id,
+ isNotFocused: true,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.availableWorkItems.loading;
+ },
+ listboxText() {
+ return (
+ this.workItems.filter((item) => this.localSelectedItem === item.value)?.[0]?.text ||
+ this.parent?.title ||
+ this.$options.i18n.none
+ );
+ },
+ workItemsMvc2Enabled() {
+ return this.glFeatures.workItemsMvc2;
+ },
+ workItems() {
+ return this.availableWorkItems.map(({ id, title }) => ({ text: title, value: id }));
+ },
+ listboxCategory() {
+ return this.searchStarted ? 'secondary' : 'tertiary';
+ },
+ listboxClasses() {
+ return {
+ 'is-not-focused': this.isNotFocused && !this.searchStarted,
+ };
+ },
+ },
+ watch: {
+ parent: {
+ handler(newVal) {
+ this.localSelectedItem = newVal?.id;
+ },
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ apollo: {
+ availableWorkItems: {
+ query: projectWorkItemsQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ searchTerm: this.search,
+ types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ in: this.search ? 'TITLE' : undefined,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace.workItems.nodes.filter((wi) => this.workItemId !== wi.id) || [];
+ },
+ error() {
+ this.$emit('error', this.$options.i18n.workItemsFetchError);
+ },
+ },
+ },
+ methods: {
+ setSearchKey(value) {
+ this.search = value;
+ },
+ async updateParent() {
+ if (this.parent?.id === this.localSelectedItem) {
+ return;
+ }
+ this.updateInProgress = true;
+ try {
+ const {
+ data: {
+ workItemUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ hierarchyWidget: {
+ parentId:
+ this.localSelectedItem === 'no-work-item-id' ? null : this.localSelectedItem,
+ },
+ },
+ },
+ });
+
+ if (errors.length) {
+ this.$emit('error', errors.join('\n'));
+ this.localSelectedItem = this.parent?.id || 'no-work-item-id';
+ }
+ } catch (error) {
+ this.$emit('error', sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType));
+ Sentry.captureException(error);
+ } finally {
+ this.updateInProgress = false;
+ }
+ },
+ handleItemClick(item) {
+ this.localSelectedItem = item;
+ this.searchStarted = false;
+ this.search = '';
+ this.updateParent();
+ },
+ unAssignParent() {
+ this.localSelectedItem = 'no-work-item-id';
+ this.updateParent();
+ },
+ onListboxShown() {
+ this.searchStarted = true;
+ this.isNotFocused = false;
+ },
+ onListboxHide() {
+ this.searchStarted = false;
+ this.search = '';
+ this.isNotFocused = true;
+ },
+ setListboxFocused() {
+ // This is to match the caret behaviour of parent listbox
+ // to the other dropdown fields of work items
+ if (document.activeElement.parentElement.id !== 'work-item-parent-listbox-value') {
+ this.isNotFocused = true;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ class="work-item-dropdown gl-flex-nowrap"
+ data-testid="work-item-parent-form"
+ :label="$options.i18n.parentLabel"
+ label-for="work-item-parent-listbox-value"
+ label-class="gl-pb-0! gl-mt-3 gl-overflow-wrap-break work-item-field-label"
+ label-cols="3"
+ label-cols-lg="2"
+ >
+ <span
+ v-if="!canUpdate"
+ class="gl-text-secondary gl-ml-4 gl-mt-3 gl-display-inline-block gl-line-height-normal work-item-field-value"
+ data-testid="disabled-text"
+ >
+ {{ listboxText }}
+ </span>
+ <div
+ v-else
+ :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }"
+ @mouseover="isNotFocused = false"
+ @mouseleave="setListboxFocused"
+ @focusout="isNotFocused = true"
+ @focusin="isNotFocused = false"
+ >
+ <gl-collapsible-listbox
+ id="work-item-parent-listbox-value"
+ class="gl-max-w-max-content"
+ data-testid="work-item-parent-listbox"
+ block
+ searchable
+ :no-caret="isNotFocused && !searchStarted"
+ is-check-centered
+ :category="listboxCategory"
+ :searching="isLoading"
+ :header-text="$options.i18n.assignParentLabel"
+ :no-results-text="$options.i18n.noMatchingResults"
+ :loading="updateInProgress"
+ :items="workItems"
+ :toggle-text="listboxText"
+ :toggle-class="listboxClasses"
+ :selected="localSelectedItem"
+ :reset-button-label="$options.i18n.unAssign"
+ @reset="unAssignParent"
+ @search="debouncedSearchKeyUpdate"
+ @select="handleItemClick"
+ @shown="onListboxShown"
+ @hidden="onListboxHide"
+ >
+ <template #list-item="{ item }">
+ <div @click="handleItemClick(item.value, $event)">
+ {{ item.text }}
+ </div>
+ </template>
+ </gl-collapsible-listbox>
+ </div>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
new file mode 100644
index 00000000000..d242db95896
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
@@ -0,0 +1,249 @@
+<script>
+import { produce } from 'immer';
+import { GlFormGroup, GlForm, GlFormRadioGroup, GlButton, GlAlert } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import WorkItemTokenInput from '../shared/work_item_token_input.vue';
+import addLinkedItemsMutation from '../../graphql/add_linked_items.mutation.graphql';
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import {
+ LINK_ITEM_FORM_HEADER_LABEL,
+ WIDGET_TYPE_LINKED_ITEMS,
+ LINKED_ITEM_TYPE_VALUE,
+ MAX_WORK_ITEMS,
+ I18N_MAX_WORK_ITEMS_ERROR_MESSAGE,
+ I18N_MAX_WORK_ITEMS_NOTE_LABEL,
+} from '../../constants';
+
+export default {
+ components: {
+ GlForm,
+ GlButton,
+ GlFormGroup,
+ GlFormRadioGroup,
+ GlAlert,
+ WorkItemTokenInput,
+ },
+ props: {
+ workItemId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemIid: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemFullPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ childrenIds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ linkedItemType: LINKED_ITEM_TYPE_VALUE.RELATED,
+ linkedItemTypes: [
+ {
+ text: this.$options.i18n.relatedToLabel,
+ value: LINKED_ITEM_TYPE_VALUE.RELATED,
+ },
+ {
+ text: this.$options.i18n.blockingLabel,
+ value: LINKED_ITEM_TYPE_VALUE.BLOCKS,
+ },
+ {
+ text: this.$options.i18n.blockedByLabel,
+ value: LINKED_ITEM_TYPE_VALUE.BLOCKED_BY,
+ },
+ ],
+ workItemsToAdd: [],
+ error: null,
+ showWorkItemsToAddInvalidMessage: false,
+ isSubmitting: false,
+ searchInProgress: false,
+ maxWorkItems: MAX_WORK_ITEMS,
+ };
+ },
+ computed: {
+ linkItemFormHeaderLabel() {
+ return LINK_ITEM_FORM_HEADER_LABEL[this.workItemType];
+ },
+ workItemsToAddInvalidMessage() {
+ return this.$options.i18n.addChildErrorMessage;
+ },
+ isSubmitButtonDisabled() {
+ return this.workItemsToAdd.length <= 0 || !this.areWorkItemsToAddValid;
+ },
+ areWorkItemsToAddValid() {
+ return this.workItemsToAdd.length <= this.maxWorkItems;
+ },
+ errorMessage() {
+ return !this.areWorkItemsToAddValid ? this.$options.i18n.maxItemsErrorMessage : '';
+ },
+ },
+ methods: {
+ async linkWorkItem() {
+ try {
+ if (this.searchInProgress) {
+ return;
+ }
+ this.isSubmitting = true;
+ const {
+ data: {
+ workItemAddLinkedItems: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: addLinkedItemsMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ linkType: this.linkedItemType,
+ workItemsIds: this.workItemsToAdd.map((wi) => wi.id),
+ },
+ },
+ update: (
+ cache,
+ {
+ data: {
+ workItemAddLinkedItems: { workItem },
+ },
+ },
+ ) => {
+ const queryArgs = {
+ query: workItemByIidQuery,
+ variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
+ };
+ const sourceData = cache.readQuery(queryArgs);
+
+ if (!sourceData) {
+ return;
+ }
+
+ cache.writeQuery({
+ ...queryArgs,
+ data: produce(sourceData, (draftState) => {
+ const linkedItemsWidget = draftState.workspace.workItems.nodes[0].widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS,
+ );
+
+ linkedItemsWidget.linkedItems = workItem.widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS,
+ ).linkedItems;
+ }),
+ });
+ },
+ });
+
+ if (errors.length > 0) {
+ [this.error] = errors;
+ return;
+ }
+
+ this.workItemsToAdd = [];
+ this.unsetError();
+ this.showWorkItemsToAddInvalidMessage = false;
+ this.linkedItemType = LINKED_ITEM_TYPE_VALUE.RELATED;
+ this.$emit('submitted');
+ } catch (e) {
+ this.error = this.$options.i18n.addLinkedItemErrorMessage;
+ } finally {
+ this.isSubmitting = false;
+ }
+ },
+ unsetError() {
+ this.error = null;
+ },
+ },
+ i18n: {
+ addButtonLabel: __('Add'),
+ relatedToLabel: s__('WorkItem|relates to'),
+ blockingLabel: s__('WorkItem|blocks'),
+ blockedByLabel: s__('WorkItem|is blocked by'),
+ linkItemInputLabel: s__('WorkItem|the following item(s)'),
+ addLinkedItemErrorMessage: s__(
+ 'WorkItem|Something went wrong when trying to link a item. Please try again.',
+ ),
+ maxItemsNoteLabel: I18N_MAX_WORK_ITEMS_NOTE_LABEL,
+ maxItemsErrorMessage: I18N_MAX_WORK_ITEMS_ERROR_MESSAGE,
+ },
+};
+</script>
+
+<template>
+ <gl-form
+ class="gl-new-card-add-form"
+ data-testid="link-work-item-form"
+ @submit.stop.prevent="linkWorkItem"
+ >
+ <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
+ {{ error }}
+ </gl-alert>
+ <gl-form-group
+ :label="linkItemFormHeaderLabel"
+ label-for="linked-item-type-radio"
+ label-class="label-bold"
+ class="gl-mb-3"
+ >
+ <gl-form-radio-group
+ id="linked-item-type-radio"
+ v-model="linkedItemType"
+ :options="linkedItemTypes"
+ :checked="linkedItemType"
+ />
+ </gl-form-group>
+ <p class="gl-font-weight-bold gl-mb-2">
+ {{ $options.i18n.linkItemInputLabel }}
+ </p>
+ <div class="gl-mb-5">
+ <work-item-token-input
+ v-model="workItemsToAdd"
+ class="gl-mb-2"
+ :parent-work-item-id="workItemId"
+ :children-ids="childrenIds"
+ :are-work-items-to-add-valid="areWorkItemsToAddValid"
+ :full-path="workItemFullPath"
+ :max-selection-limit="maxWorkItems"
+ @searching="searchInProgress = $event"
+ />
+ <div v-if="errorMessage" class="gl-mb-2 gl-text-red-500">
+ {{ $options.i18n.maxItemsErrorMessage }}
+ </div>
+ <div v-if="!errorMessage" data-testid="max-work-item-note" class="gl-text-gray-500">
+ {{ $options.i18n.maxItemsNoteLabel }}
+ </div>
+ <div
+ v-if="showWorkItemsToAddInvalidMessage"
+ class="gl-text-red-500"
+ data-testid="work-items-invalid"
+ >
+ {{ workItemsToAddInvalidMessage }}
+ </div>
+ </div>
+ <gl-button
+ data-testid="link-work-item-button"
+ category="primary"
+ variant="confirm"
+ size="small"
+ type="submit"
+ :disabled="isSubmitButtonDisabled"
+ :loading="isSubmitting"
+ class="gl-mr-2"
+ >
+ {{ $options.i18n.addButtonLabel }}
+ </gl-button>
+ <gl-button category="secondary" size="small" @click="$emit('cancel')">
+ {{ s__('WorkItem|Cancel') }}
+ </gl-button>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
index cbe830f9565..002c1786044 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
@@ -1,6 +1,5 @@
<script>
import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
-import { workItemPath } from '../../utils';
export default {
components: {
@@ -20,20 +19,11 @@ export default {
type: Boolean,
required: true,
},
- workItemFullPath: {
- type: String,
- required: true,
- },
- },
- methods: {
- linkedItemPath(fullPath, id) {
- return workItemPath(fullPath, id);
- },
},
};
</script>
<template>
- <div>
+ <div data-testid="work-item-linked-items-list">
<h4
v-if="heading"
data-testid="work-items-list-heading"
@@ -51,8 +41,9 @@ export default {
<work-item-link-child-contents
:child-item="linkedItem.workItem"
:can-update="canUpdate"
- :child-path="linkedItemPath(workItemFullPath, linkedItem.workItem.iid)"
+ :show-task-icon="true"
@click="$emit('showModal', { event: $event, child: linkedItem.workItem })"
+ @removeChild="$emit('removeLinkedItem', linkedItem.workItem)"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
index 4f6879e9605..20427fe96c4 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
@@ -1,23 +1,37 @@
<script>
-import { GlLoadingIcon, GlIcon, GlButton } from '@gitlab/ui';
+import { produce } from 'immer';
+import { GlLoadingIcon, GlIcon, GlButton, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import removeLinkedItemsMutation from '../../graphql/remove_linked_items.mutation.graphql';
import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemRelationshipList from './work_item_relationship_list.vue';
+import WorkItemAddRelationshipForm from './work_item_add_relationship_form.vue';
export default {
+ helpPath: helpPagePath('/user/okrs.md#linked-items-in-okrs'),
components: {
GlLoadingIcon,
GlIcon,
GlButton,
+ GlLink,
WidgetWrapper,
WorkItemRelationshipList,
+ WorkItemAddRelationshipForm,
},
+ inject: ['isGroup'],
props: {
+ workItemId: {
+ type: String,
+ required: false,
+ default: null,
+ },
workItemIid: {
type: String,
required: true,
@@ -26,10 +40,17 @@ export default {
type: String,
required: true,
},
+ workItemType: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
apollo: {
workItem: {
- query: workItemByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
+ },
variables() {
return {
fullPath: this.workItemFullPath,
@@ -74,13 +95,13 @@ export default {
linksRelatesTo: [],
linksIsBlockedBy: [],
linksBlocks: [],
+ isShownLinkItemForm: false,
widgetName: 'linkeditems',
};
},
computed: {
- canUpdate() {
- // This will be false untill we implement remove item mutation
- return false;
+ canAdminWorkItemLink() {
+ return this.workItem?.userPermissions?.adminWorkItemLink;
},
isLoading() {
return this.$apollo.queries.workItem.loading;
@@ -91,18 +112,88 @@ export default {
linkedWorkItems() {
return this.linkedWorkItemsWidget?.linkedItems?.nodes || [];
},
+ childrenIds() {
+ return this.linkedWorkItems.map((item) => item.workItem.id);
+ },
linkedWorkItemsCount() {
return this.linkedWorkItems.length;
},
isEmptyRelatedWorkItems() {
- return !this.error && this.linkedWorkItems.length === 0;
+ return !this.isShownLinkItemForm && !this.error && this.linkedWorkItems.length === 0;
+ },
+ },
+ methods: {
+ showLinkItemForm() {
+ this.isShownLinkItemForm = true;
+ },
+ hideLinkItemForm() {
+ this.isShownLinkItemForm = false;
+ },
+ async removeLinkedItem(linkedItem) {
+ try {
+ const {
+ data: {
+ workItemRemoveLinkedItems: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: removeLinkedItemsMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ workItemsIds: [linkedItem.id],
+ },
+ },
+ update: (cache, { data: { workItemRemoveLinkedItems } }) => {
+ const errorMessages = workItemRemoveLinkedItems?.errors;
+ if (errorMessages && errorMessages.length > 0) {
+ [this.error] = errorMessages;
+ return;
+ }
+ const queryArgs = {
+ query: workItemByIidQuery,
+ variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
+ };
+ const sourceData = cache.readQuery(queryArgs);
+
+ if (!sourceData) {
+ return;
+ }
+
+ cache.writeQuery({
+ ...queryArgs,
+ data: produce(sourceData, (draftState) => {
+ const linkedItems =
+ draftState.workspace.workItems.nodes[0].widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS,
+ )?.linkedItems?.nodes || [];
+ const index = linkedItems.findIndex((item) => {
+ return item.workItem.id === linkedItem.id;
+ });
+ linkedItems.splice(index, 1);
+ }),
+ });
+ },
+ });
+
+ if (errors.length > 0) {
+ [this.error] = errors;
+ return;
+ }
+
+ this.$toast.show(s__('WorkItem|Linked item removed'));
+ } catch {
+ this.error = this.$options.i18n.removeLinkedItemErrorMessage;
+ }
},
},
i18n: {
title: s__('WorkItem|Linked Items'),
- fetchError: s__('WorkItem|Something went wrong when fetching tasks. Please refresh this page.'),
+ fetchError: s__('WorkItem|Something went wrong when fetching items. Please refresh this page.'),
emptyStateMessage: s__(
- "WorkItem|Link work items together to show that they're related or that one is blocking others.",
+ "WorkItem|Link items together to show that they're related or that one is blocking others.",
+ ),
+ removeLinkedItemErrorMessage: s__(
+ 'WorkItem|Something went wrong when removing item. Please refresh this page.',
),
addChildButtonLabel: s__('WorkItem|Add'),
relatedToTitle: s__('WorkItem|Related to'),
@@ -131,17 +222,36 @@ export default {
</div>
</template>
<template #header-right>
- <gl-button size="small" class="gl-ml-3">
+ <gl-button
+ v-if="canAdminWorkItemLink"
+ data-testid="link-item-add-button"
+ size="small"
+ class="gl-ml-3"
+ @click="showLinkItemForm"
+ >
<slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot>
</gl-button>
</template>
<template #body>
<div class="gl-new-card-content">
+ <work-item-add-relationship-form
+ v-if="isShownLinkItemForm"
+ :work-item-id="workItemId"
+ :work-item-iid="workItemIid"
+ :work-item-full-path="workItemFullPath"
+ :children-ids="childrenIds"
+ :work-item-type="workItemType"
+ @submitted="hideLinkItemForm"
+ @cancel="hideLinkItemForm"
+ />
<gl-loading-icon v-if="isLoading" color="dark" class="gl-my-2" />
<template v-else>
- <div v-if="isEmptyRelatedWorkItems" data-testid="links-empty">
+ <div v-if="!isShownLinkItemForm && isEmptyRelatedWorkItems" data-testid="links-empty">
<p class="gl-new-card-empty">
{{ $options.i18n.emptyStateMessage }}
+ <gl-link :href="$options.helpPath" data-testid="help-link">
+ {{ __('Learn more.') }}
+ </gl-link>
</p>
</div>
<template v-else>
@@ -153,9 +263,9 @@ export default {
}"
:linked-items="linksBlocks"
:heading="$options.i18n.blockingTitle"
- :work-item-full-path="workItemFullPath"
- :can-update="canUpdate"
+ :can-update="canAdminWorkItemLink"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ @removeLinkedItem="removeLinkedItem"
/>
<work-item-relationship-list
v-if="linksIsBlockedBy.length"
@@ -165,17 +275,17 @@ export default {
}"
:linked-items="linksIsBlockedBy"
:heading="$options.i18n.blockedByTitle"
- :work-item-full-path="workItemFullPath"
- :can-update="canUpdate"
+ :can-update="canAdminWorkItemLink"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ @removeLinkedItem="removeLinkedItem"
/>
<work-item-relationship-list
v-if="linksRelatesTo.length"
:linked-items="linksRelatesTo"
:heading="$options.i18n.relatedToTitle"
- :work-item-full-path="workItemFullPath"
- :can-update="canUpdate"
+ :can-update="canAdminWorkItemLink"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ @removeLinkedItem="removeLinkedItem"
/>
</template>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_todos.vue b/app/assets/javascripts/work_items/components/work_item_todos.vue
index b21abf21be5..e6d7f2067ba 100644
--- a/app/assets/javascripts/work_items/components/work_item_todos.vue
+++ b/app/assets/javascripts/work_items/components/work_item_todos.vue
@@ -4,9 +4,10 @@ import { produce } from 'immer';
import { s__ } from '~/locale';
import { updateGlobalTodoCount } from '~/sidebar/utils';
-import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import createWorkItemTodosMutation from '~/work_items/graphql/create_work_item_todos.mutation.graphql';
-import markDoneWorkItemTodosMutation from '~/work_items/graphql/mark_done_work_item_todos.mutation.graphql';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
+import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
+import createWorkItemTodosMutation from '../graphql/create_work_item_todos.mutation.graphql';
+import markDoneWorkItemTodosMutation from '../graphql/mark_done_work_item_todos.mutation.graphql';
import {
TODO_ADD_ICON,
@@ -28,6 +29,7 @@ export default {
GlIcon,
GlButton,
},
+ inject: ['isGroup'],
props: {
workItemId: {
type: String,
@@ -148,7 +150,7 @@ export default {
},
updateWorkItemCurrentTodosWidgetCache({ cache, todos }) {
const query = {
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.workItemFullpath, iid: this.workItemIid },
};
diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
index 5426f3965b3..76a73093206 100644
--- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue
+++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
@@ -36,6 +36,11 @@ export default {
return this.workItemType.toUpperCase().split(' ').join('_');
},
iconName() {
+ // TODO Delete this conditional once we have an `issue-type-epic` icon
+ if (this.workItemIconName === 'issue-type-epic') {
+ return 'epic';
+ }
+
return (
this.workItemIconName ||
WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.icon ||
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 2b118247426..a64172acff4 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -112,8 +112,19 @@ export const I18N_WORK_ITEM_COPY_CREATE_NOTE_EMAIL = s__(
'WorkItem|Copy %{workItemType} email address',
);
+export const MAX_WORK_ITEMS = 10;
+
+export const I18N_MAX_WORK_ITEMS_ERROR_MESSAGE = sprintf(
+ s__('WorkItem|Only %{MAX_WORK_ITEMS} items can be added at a time.'),
+ { MAX_WORK_ITEMS },
+);
+export const I18N_MAX_WORK_ITEMS_NOTE_LABEL = sprintf(
+ s__('WorkItem|Add a maximum of %{MAX_WORK_ITEMS} items at a time.'),
+ { MAX_WORK_ITEMS },
+);
+
export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => {
- const workItemType = workItemTypeArg || s__('WorkItem|Work item');
+ const workItemType = workItemTypeArg || s__('WorkItem|item');
return capitalizeFirstCharacter(
sprintf(msg, {
workItemType: workItemType.toLocaleLowerCase(),
@@ -186,8 +197,11 @@ export const WORK_ITEM_NAME_TO_ICON_MAP = {
Issue: 'issue-type-issue',
Task: 'issue-type-task',
Objective: 'issue-type-objective',
+ Incident: 'issue-type-incident',
// eslint-disable-next-line @gitlab/require-i18n-strings
'Key Result': 'issue-type-keyresult',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ 'Test Case': 'issue-type-test-case',
};
export const FORM_TYPES = {
@@ -262,3 +276,15 @@ export const LINKED_CATEGORIES_MAP = {
IS_BLOCKED_BY: 'is_blocked_by',
BLOCKS: 'blocks',
};
+
+export const LINKED_ITEM_TYPE_VALUE = {
+ RELATED: 'RELATED',
+ BLOCKED_BY: 'BLOCKED_BY',
+ BLOCKS: 'BLOCKS',
+};
+
+export const LINK_ITEM_FORM_HEADER_LABEL = {
+ [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: s__('WorkItem|The current objective'),
+ [WORK_ITEM_TYPE_VALUE_KEY_RESULT]: s__('WorkItem|The current key result'),
+ [WORK_ITEM_TYPE_VALUE_TASK]: s__('WorkItem|The current task'),
+};
diff --git a/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql b/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql
new file mode 100644
index 00000000000..ba12c7f9b51
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./work_item.fragment.graphql"
+
+mutation addLinkedItems($input: WorkItemAddLinkedItemsInput!) {
+ workItemAddLinkedItems(input: $input) {
+ workItem {
+ ...WorkItem
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/cache_utils.js b/app/assets/javascripts/work_items/graphql/cache_utils.js
index 14eedf5cdd8..aeeffea24e7 100644
--- a/app/assets/javascripts/work_items/graphql/cache_utils.js
+++ b/app/assets/javascripts/work_items/graphql/cache_utils.js
@@ -1,5 +1,6 @@
import { produce } from 'immer';
import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
+import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { findHierarchyWidgetChildren } from '~/work_items/utils';
@@ -127,8 +128,11 @@ export const updateCacheAfterRemovingAwardEmojiFromNote = (currentNotes, note) =
});
};
-export const addHierarchyChild = (cache, fullPath, iid, workItem) => {
- const queryArgs = { query: workItemByIidQuery, variables: { fullPath, iid } };
+export const addHierarchyChild = ({ cache, fullPath, iid, isGroup, workItem }) => {
+ const queryArgs = {
+ query: isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
+ variables: { fullPath, iid },
+ };
const sourceData = cache.readQuery(queryArgs);
if (!sourceData) {
@@ -143,8 +147,11 @@ export const addHierarchyChild = (cache, fullPath, iid, workItem) => {
});
};
-export const removeHierarchyChild = (cache, fullPath, iid, workItem) => {
- const queryArgs = { query: workItemByIidQuery, variables: { fullPath, iid } };
+export const removeHierarchyChild = ({ cache, fullPath, iid, isGroup, workItem }) => {
+ const queryArgs = {
+ query: isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
+ variables: { fullPath, iid },
+ };
const sourceData = cache.readQuery(queryArgs);
if (!sourceData) {
diff --git a/app/assets/javascripts/work_items/graphql/group_work_item_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/group_work_item_by_iid.query.graphql
new file mode 100644
index 00000000000..f23bafa20c3
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_work_item_by_iid.query.graphql
@@ -0,0 +1,12 @@
+#import "./work_item.fragment.graphql"
+
+query groupWorkItemByIid($fullPath: ID!, $iid: String) {
+ workspace: group(fullPath: $fullPath) @persist {
+ id
+ workItems(iid: $iid) {
+ nodes {
+ ...WorkItem
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
index 7d63af448d4..2be436aa8c2 100644
--- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
@@ -9,6 +9,7 @@ query projectWorkItems(
workItems(search: $searchTerm, types: $types, in: $in) {
nodes {
id
+ iid
title
state
confidential
diff --git a/app/assets/javascripts/work_items/graphql/remove_linked_items.mutation.graphql b/app/assets/javascripts/work_items/graphql/remove_linked_items.mutation.graphql
new file mode 100644
index 00000000000..f83f5474606
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/remove_linked_items.mutation.graphql
@@ -0,0 +1,6 @@
+mutation removeLinkedItems($input: WorkItemRemoveLinkedItemsInput!) {
+ workItemRemoveLinkedItems(input: $input) {
+ errors
+ message
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql
index f28317b79b5..9d71d452430 100644
--- a/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/update_work_item_notifications.mutation.graphql
@@ -1,9 +1,14 @@
-mutation updateWorkItemNotificationsWidget($input: IssueSetSubscriptionInput!) {
- updateWorkItemNotificationsSubscription: issueSetSubscription(input: $input) {
- issue {
+mutation workItemSubscribe($input: WorkItemSubscribeInput!) {
+ workItemSubscribe(input: $input) {
+ errors
+ workItem {
id
- subscribed
+ widgets {
+ ... on WorkItemWidgetNotifications {
+ type
+ subscribed
+ }
+ }
}
- errors
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index 1ae5617f04d..fac99310890 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -33,6 +33,7 @@ fragment WorkItem on WorkItem {
adminParentLink
setWorkItemMetadata
createNote
+ adminWorkItemLink
}
widgets {
...WorkItemWidgets
diff --git a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
index f303a797e9c..d15e3086560 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
@@ -52,4 +52,12 @@ fragment WorkItemMetadataWidgets on WorkItemWidget {
... on WorkItemWidgetAwardEmoji {
type
}
+
+ ... on WorkItemWidgetLinkedItems {
+ type
+ }
+
+ ... on WorkItemWidgetHierarchy {
+ type
+ }
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
index b4fb83b24c2..5c797367903 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
@@ -37,6 +37,7 @@ query workItemTreeQuery($id: WorkItemID!) {
state
createdAt
closedAt
+ webUrl
widgets {
... on WorkItemWidgetHierarchy {
type
diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
index ffc9fe2f7f7..b357e765d16 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
@@ -66,6 +66,7 @@ fragment WorkItemWidgets on WorkItemWidget {
state
createdAt
closedAt
+ webUrl
widgets {
... on WorkItemWidgetHierarchy {
type
@@ -120,6 +121,7 @@ fragment WorkItemWidgets on WorkItemWidget {
state
createdAt
closedAt
+ webUrl
widgets {
...WorkItemMetadataWidgets
}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index 70bda7d3783..0b7f9290d6e 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -1,17 +1,25 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { WORKSPACE_GROUP } from '~/issues/constants';
import { parseBoolean } from '~/lib/utils/common_utils';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import App from './components/app.vue';
+import WorkItemRoot from './pages/work_item_root.vue';
import { createRouter } from './router';
Vue.use(VueApollo);
-export const initWorkItemsRoot = () => {
+export const initWorkItemsRoot = (workspace) => {
const el = document.querySelector('#js-work-items');
+
+ if (!el) {
+ return undefined;
+ }
+
const {
fullPath,
hasIssueWeightsFeature,
+ iid,
issuesListPath,
registerPath,
signInPath,
@@ -22,6 +30,8 @@ export const initWorkItemsRoot = () => {
reportAbusePath,
} = el.dataset;
+ const Component = workspace === WORKSPACE_GROUP ? WorkItemRoot : App;
+
return new Vue({
el,
name: 'WorkItemsRoot',
@@ -29,6 +39,7 @@ export const initWorkItemsRoot = () => {
apolloProvider,
provide: {
fullPath,
+ isGroup: workspace === WORKSPACE_GROUP,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasOkrsFeature: parseBoolean(hasOkrsFeature),
issuesListPath,
@@ -40,7 +51,11 @@ export const initWorkItemsRoot = () => {
reportAbusePath,
},
render(createElement) {
- return createElement(App);
+ return createElement(Component, {
+ props: {
+ iid: workspace === WORKSPACE_GROUP ? iid : undefined,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index b5705b21b5a..31e790254d9 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -10,6 +10,7 @@ import {
} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
+import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import ItemTitle from '../components/item_title.vue';
@@ -22,7 +23,7 @@ export default {
ItemTitle,
GlFormSelect,
},
- inject: ['fullPath'],
+ inject: ['fullPath', 'isGroup'],
props: {
initialTitle: {
type: String,
@@ -94,7 +95,7 @@ export default {
const { workItem } = workItemCreate;
store.writeQuery({
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: {
fullPath: this.fullPath,
iid: workItem.iid,
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 1443e4b509d..ac5d8b32fad 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,4 +1,3 @@
-import { joinPaths } from '~/lib/utils/url_utility';
import {
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_HEALTH_STATUS,
@@ -43,7 +42,3 @@ export const markdownPreviewPath = (fullPath, iid) =>
`${
gon.relative_url_root || ''
}/${fullPath}/preview_markdown?target_type=WorkItem&target_id=${iid}`;
-
-export const workItemPath = (fullPath, workItemIid) => {
- return joinPaths(gon?.relative_url_root || '/', fullPath, '-', 'work_items', workItemIid);
-};